Github's Codespaces

For completeness, here is a working solution:

devcontainer.json

{
    "dockerComposeFile": "docker-compose.yml",
    "workspaceFolder": "/workspaces/plfantasy",
    "service": "plfantasy",
    "shutdownAction": "stopCompose",
    "features": {
        "ghcr.io/devcontainers/features/common-utils:2": {
            "installZsh": "true",
            "username": "vscode",
            "userUid": "1000",
            "userGid": "1000",
            "upgradePackages": "true"
        },
        "ghcr.io/devcontainers/features/git:1": {
            "version": "latest",
            "ppa": "false"
        }
    },
    // Configure tool-specific properties.
    "customizations": {
        // Configure properties specific to VS Code.
        "vscode": {
            // Set *default* container specific settings.json values on container create.
            "settings": {
                "emmet.includeLanguages": {
                    "phoenix-heex": "html"
                },
                "tailwindCSS.includeLanguages": {
                    "elixir": "html",
                    "phoenix-heex": "html"
                }
            },
            "extensions": [
                "jakebecker.elixir-ls",
                "phoenixframework.phoenix",
                "samuel-pordeus.elixir-test",
                "bradlc.vscode-tailwindcss",
                "ms-azuretools.vscode-docker",
                "ms-vscode.azurecli",
                "eamodio.gitlens"
            ]
        }
    },
    // Use 'forwardPorts' to make a list of ports inside the container available locally.
    "forwardPorts": [
        4000,
        4001,
        5432
    ],
    // Use 'postCreateCommand' to run commands after the container is created.
    "postCreateCommand": "mix local.hex --force && mix local.rebar --force && mix archive.install --force hex phx_new",
    // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
    "remoteUser": "vscode"
}

docker-compose.yml

version: '3'
services:
  plfantasy:
    build:
      context: ..
      dockerfile: .devcontainer/Dockerfile

    volumes:
      - ../..:/workspaces:cached

    # Overrides default command so things don't shut down after the process ends.
    command: sleep infinity

    environment:
      MIX_ENV: dev

    # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
    network_mode: service:db

    # Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
    # (Adding the "ports" property to this file will not forward from a Codespace.)

  db:
    image: postgres:latest
    restart: unless-stopped
    environment:
      POSTGRES_USER: postgres
      POSTGRES_DB: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
      - postgres-data:/var/lib/postgresql/data
volumes:
  postgres-data:

Dockerfile

FROM hexpm/elixir:1.15.7-erlang-26.1.2-debian-buster-20231009-slim AS elixir-build

RUN apt-get update && \
  apt-get install -y git && \
  apt-get install -y postgresql-client && \
  apt-get install -y inotify-tools && \
  apt-get install -y curl && \
  apt-get install -y wget && \
  apt-get install -y gnupg2 && \
  apt-get install -y dirmngr && \
  apt-get install -y gpg && \
  apt-get install -y gawk

Note the use of network_mode in docker-compose.yml. I did not need to tweak the IP mapping in config/dev.exs