Development in django dev container

Hi, I am using dev container in vscode on macos but am not able to develop without ever restarting and fixing my code without running container.

My thoughts are like this:
when I start typing the code, e.g. model definition, but meanwhile django process reloads the codebase in container and if there is an error I will get vs code pop up - not able to connect to the container. I have to see the docker logs, fix my code and reopen the container.

How to overcome this problem so you can actually code when container is running?

Container definition: findacoach/.devcontainer at main ¡ valasek/findacoach ¡ GitHub

I would suggest not using runserver (or runserver_plus) as the executable for the docker container in development. I’d probably use bash for that, and then either have a terminal session connected to that container in which to run runserver, or use the docker exec command on the host to run a startup script. In either case, you want a truly persistent process running as PID 1 in the container.

If you open the terminal session to the bash shell, then running runserver from that command line gives you direct visibility to the error messages being produced.

(Note, in addition to running runserver, you would probably want your startup script to run migrate as well. Whether or not you do this does depend upon how you’re managing your database in your development environment.)

1 Like

Thank you Ken. That is the way, now just to implement it.

When I change it in my start file, which is executed at the end od container startup code from

python manage.py migrate
exec python manage.py runserver_plus 0.0.0.0:8000

to

exec /bin/sh
# python manage.py migrate
# exec python manage.py runserver_plus 0.0.0.0:8000

it gives me

[3902 ms] Start: Run: docker inspect --type container 9c15518146a60fff617bc276cd0960a2cf83f722bded4f980412d587edc49b88
[3934 ms] Start: Run in container: /bin/sh
[3936 ms] Start: Run in container: uname -m
[3967 ms] Shell server terminated (code: 1, signal: null)
[3967 ms] Error response from daemon: container 9c15518146a60fff617bc276cd0960a2cf83f722bded4f980412d587edc49b88 is not running
[3967 ms] Start: Run in container:  (command -v getent >/dev/null 2>&1 && getent passwd 'dev-user' || grep -E '^dev-user|^[^:]*:[^:]*:dev-user:' /etc/passwd || true)
[3967 ms] Stdin closed!
[3973 ms] Exit code 1
[3974 ms] Command failed: /Applications/Visual Studio Code.app/Contents/Frameworks/Code Helper (Plugin).app/Contents/MacOS/Code Helper (Plugin) /Users/valasek/.vscode/extensions/ms-vscode-remote.remote-containers-0.380.0/dist/spec-node/devContainersSpecCLI.js run-user-commands --user-data-folder /Users/valasek/Library/Application Support/Code/User/globalStorage/ms-vscode-remote.remote-containers/data --container-session-data-folder /tmp/devcontainers-f91fb53f-7095-48f0-88a0-0390d3c1eb8f1724861127958 --workspace-folder /Users/valasek/Programming/findacoach --id-label devcontainer.local_folder=/Users/valasek/Programming/findacoach --id-label devcontainer.config_file=/Users/valasek/Programming/findacoach/.devcontainer/devcontainer.json --container-id 9c15518146a60fff617bc276cd0960a2cf83f722bded4f980412d587edc49b88 --log-level debug --log-format json --config /Users/valasek/Programming/findacoach/.devcontainer/devcontainer.json --default-user-env-probe loginInteractiveShell --skip-non-blocking-commands false --prebuild false --stop-for-personalization true --remote-env REMOTE_CONTAINERS_IPC=/tmp/vscode-remote-containers-ipc-b3ca0adc-1562-4ba8-a894-311b4c2b7e8e.sock --remote-env SSH_AUTH_SOCK=/tmp/vscode-ssh-auth-b3ca0adc-1562-4ba8-a894-311b4c2b7e8e.sock --remote-env REMOTE_CONTAINERS=true --mount-workspace-git-root --dotfiles-target-path ~/dotfiles
[13065 ms] Stdin closed!

But I am relatively new to docker.

Are you running this container individually or as part of a docker compose setup?

If you’re running it individually, you should probably run it with the -i -t parameters. If you’re running this as part of a docker compose, you might need to do something else, like run a never-ending script.
e.g.:

while /bin/true ; do sleep 60 ; done

(I don’t truly know what’s needed or “best” here - this is a lot of conjecture. I’m only moderately familiar with docker.)

Anyway, the basic idea is to have the container running something that isn’t going to end by itself as PID 1.

1 Like

SOLVED!
Ken, thank yout for your help. There are two solutions:

  1. keep the file start as is
#!/bin/bash

set -o errexit
set -o pipefail
set -o nounset

python manage.py migrate
exec python manage.py runserver_plus 0.0.0.0:8000

and change overrideCommand to True in devcontainer.json - this is how I did it

    // Tells devcontainer.json supporting services / tools whether they should run
    // /bin/sh -c "while sleep 1000; do :; done" when starting the container instead of the container’s default command
    "overrideCommand": true,

OR

  1. keep overrideCommand set to false and change the file start

start

#!/bin/bash

set -o errexit
set -o pipefail
set -o nounset

exec /bin/bash -c "trap : TERM INT; sleep infinity & wait"
# python manage.py migrate
# exec python manage.py runserver_plus 0.0.0.0:8000

So now, when I reopen the project in dev container, which VS Code proposes me to do, it works, VS Code opens terminal in dev container directly and I can run Django command, e.g. ‘/manage.py runserver_plus 0.0.0.0:8000’

PostgreSQL is available
Agent pid 452
dev-user@e43bdabb2e9d:/app$ './manage.py runserver_plus 0.0.0.0:8000'
bash: ./manage.py runserver_plus 0.0.0.0:8000: No such file or directory
dev-user@e43bdabb2e9d:/app$ ./manage.py runserver_plus 0.0.0.0:8000
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:8000
 * Running on http://172.20.0.3:8000
Press CTRL+C to quit
 * Restarting with watchdog (inotify)
Performing system checks...

System check identified no issues (0 silenced).

Django version 5.1, using settings 'config.settings.local'
Development server is running at http://0.0.0.0:8000/
Using the Werkzeug debugger (https://werkzeug.palletsprojects.com/)
Quit the server with CONTROL-C.
 * Debugger is active!
 * Debugger PIN: 264-407-905

For the reference my final configuration is:

Dockerfile

# define an alias for the specific python version used in this file.
FROM docker.io/python:3.12.5-slim-bookworm AS python

# Python build stage
FROM python AS python-build-stage

ARG BUILD_ENVIRONMENT=local

# Install apt packages
RUN apt-get update && apt-get install --no-install-recommends -y \
  # dependencies for building Python packages
  build-essential \
  # psycopg dependencies
  libpq-dev

# Requirements are installed here to ensure they will be cached.
COPY ./requirements .

# Create Python Dependency and Sub-Dependency Wheels.
RUN pip wheel --wheel-dir /usr/src/app/wheels  \
  -r ${BUILD_ENVIRONMENT}.txt


# Python 'run' stage
FROM python AS python-run-stage

ARG BUILD_ENVIRONMENT=local
ARG APP_HOME=/app

ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
ENV BUILD_ENV=${BUILD_ENVIRONMENT}

WORKDIR ${APP_HOME}


# devcontainer dependencies and utils
RUN apt-get update && apt-get install --no-install-recommends -y \
  sudo git bash-completion nano ssh

# Create devcontainer user and add it to sudoers
RUN groupadd --gid 1000 dev-user \
  && useradd --uid 1000 --gid dev-user --shell /bin/bash --create-home dev-user \
  && echo dev-user ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/dev-user \
  && chmod 0440 /etc/sudoers.d/dev-user


# Install required system dependencies
RUN apt-get update && apt-get install --no-install-recommends -y \
  # psycopg dependencies
  libpq-dev \
  # Translations dependencies
  gettext \
  # cleaning up unused files
  && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
  && rm -rf /var/lib/apt/lists/*

# All absolute dir copies ignore workdir instruction. All relative dir copies are wrt to the workdir instruction
# copy python dependency wheels from python-build-stage
COPY --from=python-build-stage /usr/src/app/wheels  /wheels/

# use wheels to install python dependencies
RUN pip install --no-cache-dir --no-index --find-links=/wheels/ /wheels/* \
  && rm -rf /wheels/

COPY ./compose/production/django/entrypoint /entrypoint
RUN sed -i 's/\r$//g' /entrypoint
RUN chmod +x /entrypoint

COPY ./compose/local/django/start /start
RUN sed -i 's/\r$//g' /start
RUN chmod +x /start



# copy application code to WORKDIR
COPY . ${APP_HOME}

ENTRYPOINT ["/entrypoint"]

docker-compose.local

volumes:
  findacoach_local_postgres_data: {}
  findacoach_local_postgres_data_backups: {}


services:
  django:
    build:
      context: .
      dockerfile: ./compose/local/django/Dockerfile
    image: findacoach_local_django
    container_name: findacoach_local_django
    depends_on:
      - postgres
    volumes:
      - .:/app:z
    env_file:
      - ./.envs/.local/.django
      - ./.envs/.local/.postgres
    ports:
      - '8000:8000'
    command: /start

  postgres:
    build:
      context: .
      dockerfile: ./compose/production/postgres/Dockerfile
    image: findacoach_production_postgres
    container_name: findacoach_local_postgres
    volumes:
      - findacoach_local_postgres_data:/var/lib/postgresql/data
      - findacoach_local_postgres_data_backups:/backups
    env_file:
      - ./.envs/.local/.postgres

and dev container.json

// For format details, see https://containers.dev/implementors/json_reference/
{
    "name": "findacoach_dev",
    "dockerComposeFile": [
        "../docker-compose.local.yml"
    ],
    "init": true,
    "mounts": [
        {
            "source": "./.devcontainer/bash_history",
            "target": "/home/dev-user/.bash_history",
            "type": "bind"
        },
        {
            "source": "~/.ssh",
            "target": "/home/dev-user/.ssh",
            "type": "bind"
        }
    ],
    // Tells devcontainer.json supporting services / tools whether they should run
    // /bin/sh -c "while sleep 1000; do :; done" when starting the container instead of the container’s default command
    "overrideCommand": true,
    "service": "django",
    // "remoteEnv": {"PATH": "/home/dev-user/.local/bin:${containerEnv:PATH}"},
    "remoteUser": "dev-user",
    "workspaceFolder": "/app",
    // Set *default* container specific settings.json values on container create.
    "customizations": {
        "vscode": {
            "settings": {
                "editor.formatOnSave": true,
                "[python]": {
                    "analysis.autoImportCompletions": true,
                    "analysis.typeCheckingMode": "basic",
                    "defaultInterpreterPath": "/usr/local/bin/python",
                    "editor.codeActionsOnSave": {
                        "source.organizeImports": "always"
                    },
                    "editor.defaultFormatter": "charliermarsh.ruff",
                    "languageServer": "Pylance",
                    "linting.enabled": true,
                    "linting.mypyEnabled": true,
                    "linting.mypyPath": "/usr/local/bin/mypy"
                }
            },
            // https://code.visualstudio.com/docs/remote/devcontainerjson-reference#_vs-code-specific-properties
            // Add the IDs of extensions you want installed when the container is created.
            "extensions": [
                "davidanson.vscode-markdownlint",
                "mrmlnc.vscode-duplicate",
                "visualstudioexptteam.vscodeintellicode",
                "visualstudioexptteam.intellicode-api-usage-examples",
                // python
                "ms-python.python",
                "ms-python.vscode-pylance",
                "charliermarsh.ruff",
                // django
                "batisteo.vscode-django"
            ]
        }
    },
    // Uncomment the next line if you want start specific services in your Docker Compose config.
    // "runServices": [],
    // Uncomment the next line if you want to keep your containers running after VS Code shuts down.
    // "shutdownAction": "none",
    // Uncomment the next line to run commands after the container is created.
    "postCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder} && cat .devcontainer/bashrc.override.sh >> ~/.bashrc"
}