<!--
  canonical-url: https://www.avonture.be/blog/docker-prod-devcontainer
  published:     2025-10-13
  This is a static plain-Markdown mirror generated at build time.
  Visit the canonical URL above for the fully rendered page, with images and interactive components.
-->

# One Docker Image for Production and Devcontainers - The Clean Way

> Learn how to design a Docker setup that delivers a secure, minimal production image while enabling effortless local development through VS Code Devcontainers with clean overrides and environment isolation

---
![One Docker Image for Production and Devcontainers - The Clean Way](/img/v2/docker_workflow_prod_devcontainer.webp)

**TL;DR**

This article presents a clean method for using a single Docker image for both production deployments and VS Code Devcontainers. The approach involves building a minimal, secure production image first, then extending it with development tools, specific user configurations, and overrides for a separate devcontainer image. This ensures a lightweight production environment with a reduced attack surface, while providing a rich and isolated development experience.

This article is the result of intense reflection: how to define a Docker image for deployment in production as cleanly as possible and, while avoiding copy/paste and other repetitions as much as possible, create an image for development in devcontainer.

In other words: I want to create the lightest and most secure image possible for deployment in production, but of course, I need to be able to develop this image in my VSCode environment, and for that, I need my development tools, a specific Linux user to synchronize files with my host and avoid permissions problems, etc.

After multiple attempts, here is what I came up with, which seems to meet this need.

> **A few core concepts to keep in mind while reading this article:**
>
> 1. We must completely separate future production from our development (devcontainer).
> 2. We will need to create the production Docker image before we can use the devcontainer.

## What does a perfect separation of environments mean?

Our first instinct should always be: do we need this (utility, configuration, user, etc.) in the Docker image that we are going to deploy on the server?

Some examples:

* Development tools such as a linter, formatter, [code quality tool](/blog/php-jakzal-phpqa), debugger, etc. have no place in the image that will be deployed.
* Creating a user that will allow file synchronization with the host has no place in the image that will be deployed.
* ...

Our image must be clean, as efficient and lightweight as possible... We must minimize what is known as "attack surface" i.e. to reduce the number of tools we'll install in order to reduce security risks.

Everything we need for our development will then have to be part of a second Docker image, which we will use when we activate the devcontainer feature in VSCode.  This is where we will create our user, add development tools, and activate certain configurations such as debugging, etc. If you have never set one up before, [PHP development in a devcontainer with preinstalled code quality tools](/blog/vscode-devcontainer) walks through a complete devcontainer from scratch.

> **So, in short, we'll have two Docker images:**
>
> One for the deploy and a second one to extend the first with additional tools and configuration items. That one will never leave our host and we'll never build it ourselves but let VSCode do it for us.

![Core concept - Two images](./images/production_devcontainer.webp)

## The Result

One Dockerfile, no copy-paste, and both images build from the exact same instructions: a minimal, rootless production container, and a devcontainer that extends it with the tools you need to develop.

Here's the production container answering on `http://localhost:`8000:

> Screenshot — http\://localhost:8000
>
> {"message":"Hello, FastAPI - PRODUCTION!"}

![The production image has been created](./images/prod_image_created.webp)

The steps below build exactly this, from an empty folder, then extend it into a devcontainer.

## Step 1 - We have to create the production docker image

Please create a new folder in your temporary folder for our example.

```bash
$ mkdir -p /tmp/docker-prod-devcontainer && cd $_
```

### Let's create our production files

Let's create a few files (remember, for the production Docker image).

#### The production .env file

For this article, we'll create a Python script so we'll have to specify the version of Python we want.

This is just for the example but, in your case, this `.env` will be used to put there all the settings you want for your own production image.

So, even if you don't see yet why you have to use this file in your own use case, just keep it even if you have no variables to put inside.

**/tmp/docker-prod-devcontainer/.env**

```
APP_HOME="/app"
PYTHON_VERSION=3.13
TIMEZONE="Europe/Brussels"
```

#### The production Dockerfile file

The provided file is an example you can reuse. During this article, as an example, we'll create a small Python script to illustrate some things.

**/tmp/docker-prod-devcontainer/Dockerfile**

```docker
# cspell:disable

# Production Dockerfile

ARG DOCKER_APP_HOME="/app"
ARG PYTHON_VERSION=3.13
ARG TIMEZONE="Europe/Brussels"

FROM python:${PYTHON_VERSION}-slim

ARG TIMEZONE

ENV TZ=${TIMEZONE}

RUN --mount=type=cache,target=/var/cache/apt,rw \
    set -eux \
    && printf "\e[0;105m%s\e[0;0m\n" "Install required Linux binaries..." \
    && apt-get update -yqq \
    && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
        locales \
        tini \
    && echo "en_US.UTF-8 UTF-8" > /etc/locale.gen \
    && locale-gen en_US.UTF-8 \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

ENV LANG=en_US.UTF-8 \
    LANGUAGE=en_US:en \
    LC_ALL=en_US.UTF-8

ARG DOCKER_APP_HOME

WORKDIR "${DOCKER_APP_HOME}"

COPY requirements.txt .

RUN --mount=type=cache,target=/root/.cache/pip \
    set -eux \
    && printf "\e[0;105m%s\e[0;0m\n" "Install Python prod dependencies..." \
    && pip install -r requirements.txt

# Make sure installed dependencies are in the PATH
ENV PATH="/root/.local/bin:$PATH"

COPY . .

ENTRYPOINT ["/usr/bin/tini", "--"]

ENV PYTHONPATH="${DOCKER_APP_HOME}/src"

CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "--worker-class", "uvicorn.workers.UvicornWorker", "main:app"]
```

Look at the provided script:

1. We'll define the version to use for Python

2. We'll configure some environment variables like the time zone, ...

3. We'll install Linux dependencies we need for the production (**but make sure to not add here dependencies we just need for the local development**)

4. Because it's a Python example, we'll copy the `requirements.txt` file in the Docker image and install packages

5. We'll copy all our files inside the Docker image (make sure to correctly configure your `.dockerignore` file)

6. And, finally, for the production image, we'll initialize the entry point and the command to keep

> **Caution:**
>
> As you can see, we've not created here, in the Docker image, an unprivileged user. Our production image is thus violating a rule we all have read: **Your Docker image should be rootless**.
>
> This is a key concept here:
>
> * The user will be configured while deploying the image using tools like Kubernetes f.i.,
> * When we'll use the Docker image for local development, the user UID/GID has to match the one of the developers (and we don't know right now if it's `1000:1000` or something else),
> * The user can be easily specified in the `compose.yaml` file.

#### The production compose.yaml file

The `compose.yaml` file is used by Docker compose (which is a part of Docker) to know how to build your image and how to create your container.

This is a very important file since we'll define here all the required configurations, we'll specify our `.env` file so Docker can translate variables to their values, ...

We'll also tell Docker which port of the running container has to be shared with our host, ...

**/tmp/docker-prod-devcontainer/compose.yaml**

```yaml
name: my_sample

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        - DOCKER_APP_HOME=${APP_HOME:-/app}
        - PYTHON_VERSION=${PYTHON_VERSION:-3.13}
        - TIMEZONE=${TIMEZONE:-Europe/Brussels}
    image: my_sample_prod:latest
    container_name: app
    env_file: .env
    ports:
      - "8000:8000"
    user: "1001:1001"
    security_opt:
      - no-new-privileges:true
```

> **Caution:**
>
> Pay attention to the `user:` and `security_opt:` parts.
>
> It tells Docker to use a user having UID/GID `1001` when creating a container based on our image. This user ... didn't exist in the image so why?
>
> This is a trick to make sure the container is running rootless i.e. the user in the container is an unprivileged one.
>
> As extra security measure, we have the `security_opt: - no-new-privileges:true` line to prevent the container from gaining any additional privileges.
>
> In short: when a container will be created based on our image, the user will be able to just run what we've decided for him, nothing more.

#### The production Python requirements file

We've seen a mention to this file in our `Dockerfile`: because we're using Python as an example for this blog post, we need a `requirements.txt` file to declare which dependencies our script needs. In our example, we need FastAPI dependencies:

**/tmp/docker-prod-devcontainer/requirements.txt**

```txt
fastapi
gunicorn
uvicorn[standard]
```

> **Information:**
>
> Look at this file once more: we only have, strictly speaking, the dependencies we need for production.

If your own use case isn't a Python script, for sure, you should not create this `requirements.txt` file (but, then, also update the `Dockerfile` and remove that part).

#### The production Python script

And finally, we'll create, for the demo, a FastAPI Python script that will return a simple JSON answer; pretty straightforward:

**/tmp/docker-prod-devcontainer/src/main.py**

```python
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "Hello, FastAPI - PRODUCTION!"}
```

#### Summary for the production files

So at this point, our project looks like this i.e. we have five files.

```tree expanded=true showJSX=false debug=false title="docker-prod-devcontainer"
├─ .env
├── Dockerfile
├── compose.yaml
├── requirements.txt
└── src
    └── main.py
```

### Creating the Docker production image

Jump in a Linux console and run the command below to see if everything is fine i.e. if there are no error and f.i. our `.env` file where we've specified the version to use for Python is well retrieved and used.

```bash
$ docker compose config
```

You'll get something like this:

**console output**

```
name: my_sample
services:
  app:
    build:
      context: /tmp/docker-prod-devcontainer
      dockerfile: Dockerfile
      args:
        APP_HOME: /ap
        PYTHON_VERSION: "3.13"
        TIMEZONE: Europe/Brussels
    container_name: app
    environment:
      APP_HOME: /ap
      PYTHON_VERSION: "3.13"
      TIMEZONE: Europe/Brussels
    image: my_sample_prod:latest
    networks:
      default: null
    ports:
      - mode: ingress
        target: 8000
        published: "8000"
        protocol: tcp
    security_opt:
      - no-new-privileges:true
    user: 1001:1001
networks:
  default:
    name: my_sample_default
```

> **Remember our user 1001:**
>
> Look here above on the `user: 1001:1001` line. We've seen this in a previous chapter: when the container will be created, the active user will be `1001:1001` and that one didn't exist (we haven't created it in our `Dockerfile`): our container will be rootless; just what we want.

Now, we'll build and create the container by running the command below.

> **EXTREMELY IMPORTANT - WE HAVE TO BUILD THE IMAGE FIRST:**
>
> Because our future devcontainer will be derived from the production image, we MUST build it first:
>
> ```bash
> $ docker compose build
> ```
>
> This ensures that VSCode will retrieve a local version of your image. If you don't do this, if you've not yet built the image, Docker will try to pull it from Docker hub (from internet) and you'll probably get an error.
>
> So, once again, before going further in this tutorial, please run `docker compose build` first in a console.

### Running a production container

Now, we can create a container if we want by running the command below:

```bash
$ docker compose up --detach
```

Since our demo project is a FastAPI Python application, simply start a browser and go to the `http://localhost
:
8000` location to see the message from FastAPI. The output is a JSON one:

> Screenshot — http\://localhost:8000
>
> {"message":"Hello, FastAPI - PRODUCTION!"}

### And, just to be sure, checking who is the current user for PROD

Optional, let's run a parametrized command into the container to see who is the current user:

```bash
$ docker compose exec app /bin/sh -c 'echo "Container user ID is $(id -u) and his group ID is $(id -g)"'
Container user ID is 1001 and his group ID is 1001
```

This, just to confirm: when running our container, it'll run using a not-privileged user thanks to our `compose.yaml` orchestration file.

![The production image has been created](./images/prod_image_created.webp)

## Step 2 - Override (or extend) the production image and get a devcontainer ready one

### Let's create our devcontainer files

Please now create a subfolder called `.devcontainer` with the files below.

#### The .devcontainer/.env file

In case we need some more configuration items, let's create the `.devcontainer/.env` file (even if empty).

**/tmp/docker-prod-devcontainer/.devcontainer/.env**

```
```

#### The .devcontainer/Dockerfile file

One of the most important file is the `.devcontainer/Dockerfile` where we'll add extra things (Linux dependencies, Python libraries, ...) to allow us to have a better development experience.

**/tmp/docker-prod-devcontainer/.devcontainer/Dockerfile**

```docker
# syntax=docker/dockerfile:1.6

# .devcontainer/Dockerfile

ARG OS_USERID=1000
ARG OS_GROUPID=1000
ARG OS_USERNAME="vscode"
ARG DOCKER_APP_HOME="/app"

FROM my_sample_prod:latest

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

USER root

#! If you get an error during the execution of this block, make sure the "my_sample_prod"
#! image isn't running with a non-root user. It should be "root". Rebuild the image to be sure.
RUN --mount=type=cache,target=/var/cache/apt,rw \
    set -eux \
    && printf "\e[0;105m%s\e[0;0m\n" "Install required Linux binaries for devcontainer..." \
    && apt-get update -yqq \
    && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
        bash \
        bash-completion \
        git \
        curl \
        ca-certificates \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# --- Create non-root user ---

ARG OS_USERID
ARG OS_GROUPID
ARG OS_USERNAME

RUN set -eux \
    && if ! id -u "${OS_USERNAME}" >/dev/null 2>&1; then \
        printf "\e[0;105m%s\e[0;0m\n" "Create devcontainer user ${OS_USERNAME} (${OS_USERID}:${OS_GROUPID})..." \
        && groupadd -g ${OS_GROUPID} "${OS_USERNAME}" \
        && useradd -u ${OS_USERID} -g ${OS_GROUPID} -m --shell /bin/bash --create-home "${OS_USERNAME}" \
        && chown -R "${OS_USERNAME}":"${OS_USERNAME}" "/home/${OS_USERNAME}"; \
    fi

# Ensure vscode home folders exist and are writable. The .vscode-server folder is used by
# VSCode while installing extensions in the container. That folder has to be owner by our user; not root.
RUN set -eux \
    && mkdir -p "/home/${OS_USERNAME}/.vscode-server/bin" \
    && mkdir -p "/home/${OS_USERNAME}/.vscode-server/extensions" \
    && mkdir -p "/home/${OS_USERNAME}/.vscode-server/data" \
    && chown -R "${OS_USERNAME}":"${OS_USERNAME}" "/home/${OS_USERNAME}/.vscode-server"

# ---------- Shell and history setup -----------------------------------------
# Configure bash to use this file
RUN echo "export HISTFILE=/home/${OS_USERNAME}/.bash_history" >> "/home/${OS_USERNAME}/.bashrc" && \
    echo 'export HISTSIZE=10000' >> "/home/${OS_USERNAME}/.bashrc" && \
    echo 'export HISTFILESIZE=10000' >> "/home/${OS_USERNAME}/.bashrc" && \
    echo 'shopt -s histappend' >> "/home/${OS_USERNAME}/.bashrc"

# Set up the shell prompt
RUN set -eux && \
    echo "PS1='\n\e[0;33m🐳 \e[0;36m\$(whoami)\e[0m \w # '" >> "/home/${OS_USERNAME}/.bashrc"

# Welcome message and tips (make sure the message isn't yet in the file)
RUN set -eux; \
    BASHRC="/home/${OS_USERNAME}/.bashrc"; \
    MARKER="# DEV CONTAINER WELCOME MESSAGE"; \
    grep -qxF "$MARKER" "$BASHRC" || cat <<EOF >> "$BASHRC"
$MARKER
echo -e "\n📘 Welcome to the Dev Container!"
echo -e "Below are some useful commands:"
echo -e ""
echo -e "  ✨ \e[32mpython -m debugpy --listen 0.0.0.0:5679 -m uvicorn main:app --host 0.0.0.0 --port 8000 --reload\e[0m  Start a hot-reloading preview in your browser."
EOF

ARG DOCKER_APP_HOME

# Important: in the production image, the folder "DOCKER_APP_HOME" (/app most probably) is
# owned by the root user. Here, for the devcontainer, the folder has to be owned by our
# current user.
RUN set -eux \
    && chown -R "${OS_USERNAME}":"${OS_USERNAME}" "${DOCKER_APP_HOME}"

USER "${OS_USERNAME}"

COPY requirements.txt .

RUN --mount=type=cache,target=/home/${OS_USERNAME}/.cache/pip \
    set -eux \
    && cat requirements.txt \
    && printf "\e[0;105m%s\e[0;0m\n" "Install Python dev dependencies..." \
    && python -m pip install -r requirements.txt

WORKDIR "${DOCKER_APP_HOME}"
```

Look at the provided script once again:

1. We are reusing our previously created Docker image (see the `FROM my_sample_prod:latest` line)

2. We'll install some dev Linux tools like `bash`, `curl`, `git`, ... we don't need in prod

3. Now, we'll create a non-root user by using a few parameters like `OS_USERID`, `OS_GROUPID` and `OS_USERNAME`. We'll create some custom folders too for that user

4. We'll configure our Shell for things like the history file, to have a customized prompt, ...

5. We'll make sure the different folder we need to manipulate are owned by our user

6. We'll install some specific Python Dev dependencies (this is needed for our sample here)

This last step is thus only needed for illustration of this blog post. Please use your own.

> **Paying attention to the FROM statement:**
>
> Let me insist one more time on this point. Right now, our `Dockerfile` is extending an existing image called `my_sample_prod:latest`.
>
> Do you have that image right now on your host? If you don't have it, VSCode, when you'll use the *Open in Devcontainer* feature, will first try to download the image from Docker Hub.
>
> Ok, perhaps the image is hosted there and in that case, Docker will download it from the Hub. Perhaps that image doesn't exist elsewhere than on your host.  Perhaps the image exists on both the Hub and your computer.
>
> So many questions, right? Just be very aware of this!
>
> Make sure to run `docker compose build` in your root folder if you need to:
>
> 1. Create the local image
> 2. Refresh the local image because you've modified your production Docker files (as seen in the previous chapter).
>
> So, if not yet done, please run `docker compose build`! Make sure running `docker image ls | grep -i my_sample` in your console will well list your image.

#### The .devcontainer/requirements.txt file

This small file is there just for illustration purposes. In this blog post, we're creating a small FastAPI python application and, in the `.devcontainer/requirements.txt` we'll add some extra dev dependencies for Python, like installing the `Debugpy` tool i.e. the Microsoft Python Debugger.

**/tmp/docker-prod-devcontainer/.devcontainer/requirements.txt**

```txt
debugpy
```

Of course, if your project isn't a Python one, this file can be removed and, in that case, also remove the associated lines from the `.devcontainer/Dockerfile`.

#### The .devcontainer/compose.yaml file

As for the production image, here, we need to tell Docker how the image has to be built and how the container (the dev one) has to be created.

**/tmp/docker-prod-devcontainer/.devcontainer/compose.yaml**

```yaml
services:
  app:
    build:
      context: ./.devcontainer
      dockerfile: Dockerfile
      args:
        - DOCKER_APP_HOME=${APP_HOME:-/app}
        - OS_USERID=${LOCAL_UID:-1000}
        - OS_GROUPID=${LOCAL_GID:-1000}
        - OS_USERNAME=${LOCAL_USER:-vscode}
    container_name: my_sample_devcontainer_app
    ports:
      - 5679:5679  # Debugpy port
    env_file:
      - .env
      - .devcontainer/.env
    volumes:
      - .:${APP_HOME:-/app}
      - vscode-extensions:/home/${LOCAL_USER:-vscode}/.vscode-server/extensions
      - ./.devcontainer/history/.bash_history:/home/${LOCAL_USER:-vscode}/.bash_history:rw

    command: sleep infinity

volumes:
  vscode-extensions:
```

If you look at this `.devcontainer/compose.yaml` file, we're configuring a few things:

* The list of build arguments (`args`) should mention the user id, group id and name of our user. This is important: when we'll work inside the devcontainer, any files created in the container will be owned by this specific user. If we are using `johndoe` with `1001` for both user id and group id; then files will be synchronized on your host like that; owned by `johndoe` not you. If you are the user `1000:1000` locally, you understand that files won't be yours and you'll get permissions problems. It's something we have to avoid. For this, we have to correctly configure the `.devcontainer/compose.yaml` file.

See the syntax: `${LOCAL_UID:-1000}`. By default, the file will assign `1000` (which is often the correct ID). But first, Docker will check if a variable called `LOCAL_UID` exists on the host. If this is the case, that value will be used. So, if running `id -u` (or `id -g` for the group id) doesn't give `1000` on your host; make sure to create the `LOCAL_UID` or `LOCAL_GID` variable in your `.bashrc` file and assign them to the correct value.

* Because our current sample is about Python, we'll also expose the DebugPy standard port thanks to the line `5679:5679` so, when starting a debug session, the container will work.

* We're also referring to two `.env` files, the one from the root folder (used for the Docker PROD image) and the one from the `.devcontainer` folder

* We'll add some volumes:
  * `.:${APP_HOME:-/app}` will mount (=share) files from our project (inside VSCode) with the `${APP_HOME:-/app}` folder i.e. where the files are stored inside the Docker PROD image. Like this, files that we already copied in the PROD Docker image will just be overridden by the ones of our dev session (=> this is where the magic happens)
  * We'll also ask Docker to create a self-managed volume for the `vscode-extensions`. Thanks to this line, the first time we'll create the devcontainer, VSCode will install extensions but next time, thanks to this line, VSCode won't install extensions again, they're already present.  This `vscode-extensions` is used for data persistence.
  * And the line for `./.devcontainer/history/.bash_history` will persist the history of your command lines (in the console) in your project. So, next time you'll work inside the devcontainer, the CLI history will be already populated with what you did in the past.

#### The .devcontainer/devcontainer.json file

And, finally, the configuration file for VSCode.

In VSCode, to be able to run a `Devcontainer:` command (from the **Command Palette** (`CTRL`+`SHIFT`+`P`)), the `.devcontainer/devcontainer.json` file should be present and tells VSCode what to do.

**/tmp/docker-prod-devcontainer/.devcontainer/devcontainer.json**

```json
{
    "name": "MySample Devcontainer",
    "dockerComposeFile": [
        "../compose.yaml",
        "compose.yaml"
    ],
    "service": "app",
    "workspaceFolder": "/app",
    "remoteUser": "vscode",
    "containerUser": "vscode",
    "containerEnv": {
        "GIT_SSH_COMMAND": "ssh -i /home/vscode/.ssh/id_ed25519 -o 'UserKnownHostsFile=/dev/null'"
    },
    "initializeCommand": "mkdir -p .devcontainer/history && touch .devcontainer/history/.bash_history && echo '.bash_history' > .devcontainer/history/.gitignore",
    "customizations": {
        "vscode": {
            "settings": {
                "files.autoSave": "onFocusChange",
                "window.title": "${dirty}${rootName} ${separator} [DevContainer]",
                "workbench.colorTheme": "Visual Studio Light"
            },
            "extensions": [
                "ms-python.debugpy"
            ]
        }
    }
}
```

1. We have to use the `dockerComposeFile` item to load our two `compose.yaml` files i.e. the one of the root (the prod one) and the one from the `.devcontainer` folder. This is needed because we just need to extend the production `compose.yaml` file. We'll reuse all services and keys defined in `../compose.yaml` and just add (extend) or update (override) some

2. We need to tell VSCode which folder should be open in our VSCode Devcontainer session, it has to be `/app` (has to match the `APP_HOME` variable from `.devcontainer/compose.yaml`)

3. We'll define our remoteUser name, it'll be `vscode` (has to match the `OS_USERNAME` variable from `.devcontainer/compose.yaml`)

4. We'll also add a `GIT_SSH_COMMAND` environment variable. This is optional but it'll allow us to run a `git` command (like `git pull`) from inside our devcontainer

5. The `initializeCommand` key will create the `.devcontainer/history` folder on our host (with us as owner) just before creating the container. Like this, we'll be able to store our bash history file there (see the `volumes` entry in `.devcontainer/compose.yaml`)

6. And, finally, in the `customizations` part, we can add vscode settings and extensions we want to configure and install inside our Devcontainer. You can add a lot of settings / extensions here if you want, depending of your project's type

#### Our project's structure right now

So at this point, our project looks like this i.e. we have ten files.

```tree expanded=true showJSX=false debug=false title="docker-prod-devcontainer/"
├── .devcontainer
│   ├── .env
│   ├── Dockerfile
│   ├── compose.yaml
│   ├── devcontainer.json
│   └── requirements.txt
├── .env
├── Dockerfile
├── compose.yaml
├── requirements.txt
└── src
    └── main.py
```

### Creating the Devcontainer image

This part is pure optional, it's just to check if everything is correctly filled in.

Jump in the `.devcontainer` folder (`cd .devcontainer`) and run this command:

```bash
$ docker compose -f ../compose.yaml -f compose.yaml config
```

You'll get something like this:

**console output**

```
name: my_sample
services:
  app:
    build:
      context: /tmp/docker-prod-devcontainer/.devcontainer
      dockerfile: Dockerfile
      args:
        APP_HOME: /ap
        OS_GROUPID: "1000"
        OS_USERID: "1000"
        OS_USERNAME: vscode
        PYTHON_VERSION: "3.13"
        TIMEZONE: Europe/Brussels
    command:
      - sleep
      - infinity
    container_name: my_sample_devcontainer_app
    environment:
      APP_HOME: /ap
      PYTHON_VERSION: "3.13"
      TIMEZONE: Europe/Brussels
    image: my_sample_prod:latest
    networks:
      default: null
    ports:
      - mode: ingress
        target: 8000
        published: "8000"
        protocol: tcp
      - mode: ingress
        target: 5679
        published: "5679"
        protocol: tcp
    security_opt:
      - no-new-privileges:true
    user: 1001:1001
    volumes:
      - type: bind
        source: /tmp/docker-prod-devcontainer
        target: /ap
        bind:
          create_host_path: true
      - type: volume
        source: vscode-extensions
        target: /home/vscode/.vscode-server/extensions
        volume: {}
      - type: bind
        source: /tmp/docker-prod-devcontainer/.devcontainer/history/.bash_history
        target: /home/vscode/.bash_history
        bind:
          create_host_path: true
networks:
  default:
    name: my_sample_default
volumes:
  vscode-extensions:
    name: my_sample_vscode-extensions
```

Did you get an error? If so, you can more easily read/manage the error right now.

You can look at the configuration to see if everything looks correct i.e. if your variables have well be replaced by the values coming from the `.env` files.

![Both prod and devcontainer have been created](./images/both_created.webp)

## Opening the container using vscode

Time to open the project using the Devcontainer feature of VSCode.

```bash
$ cd /tmp/docker-prod-devcontainer && code .
```

![Opening VSCode](./images/opening_vscode.webp)

If you look at the bottom right of your screen, you'll probably see this popup:

![The devcontainer popup](./images/popup_devcontainer.webp)

Simply click on the `Reopen in Container` button or, second way, press `CTRL`+`SHIFT`+`P` to open the **Command palette** and select the "Dev Containers: Rebuild Without Cache and Reopen in Container" option.

VSCode will close the project and reopen it. It'll take more time (the first time) because it needs to create the Docker container first.

![The DevContainer is ready](./images/devcontainer.webp)

If you see the image here above, you're in the container. I've configured a light theme just to make the clear distinction between host (black theme) and devcontainer (light theme).

Bottom right, you can create a new Terminal:

![Creating a new terminal](./images/add_new_terminal.webp)

In this demo project, you can see this message; once:

![The welcome message](./images/welcome_terminal.webp)

The idea is thus to provide contextual help to the developer and to tell him, here, how he can start the FastAPI server using hot reload and with Debugpy enabled.

### Let's test

If we return to the `http://localhost
:
8000` URL we can still get the JSON answer (so the container is well running).

> Screenshot — http\://localhost:8000
>
> {"message":"Hello, FastAPI - PRODUCTION!"}

But now, in VSCode, if you edit the `src/main.py` script and update the Hello message like below:

![Updating the Hello message](./images/updated_main.webp)

You just need to save the change, go back to the browser window and refresh the page:

> Screenshot — http\://localhost:8000
>
> {"message":"Hello, FastAPI - I'm running from the Devcontainer!"}

### Let's debug

In this demo project, we've added the Debugpy dependency.

We just need to add an extra file for VSCode.

**/tmp/docker-prod-devcontainer/.vscode/launch.json**

```json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: FastAPI",
            "type": "debugpy",
            "request": "attach",
            "connect":  {
                "host": "localhost",
                "port": 5679,
            },
            "justMyCode": true,
            "pathMappings": [
                {
                    "localRoot": "${workspaceFolder}/src",
                    "remoteRoot": "."
                }
            ]
        }
    ]
}
```

To be sure the extension is correctly working, just close VSCode and open it again. Open the DevContainer again too (now, you can simply choose "Reopen in Devcontainer").

Once in your devcontainer, please open the `src/main.py` file. Click anywhere on the return line (line 7) and press `F9` to add a breakpoint.

![Adding a breakpoint](./images/adding_a_breakpoint.webp)

Press `F5` to start the debugger session.

Go back to the browser, refresh the page and VSCode will break on that specific line.

![Success](./images/success.webp)

## Going further

The example above uses Python; the exact same two-image approach is used, language by language, in [Docker - Python devcontainer](/blog/docker-python-devcontainer) and [PHP development in a devcontainer with preinstalled code quality tools](/blog/php-devcontainer). And if a devcontainer is still an abstract notion for you, start with [Docker - Explain me like I'm five - What's Docker for?](/blog/docker-definition-like-im-five).
