(2)Creating the Pinnacle of Niche Software: The devcontainer
Source: Dev.to
Overview
I prefer an ultra‑stable, shareable, and reproducible environment, so my go‑to solution is a devcontainer — see the official docs.
Until recently I treated the .devcontainer folder as an intrinsic part of the project, containing the single “true” development container setup. Developers could bring their own custom configs via dotfiles.
My perspective has shifted. Now I add a folder with my own name inside .devcontainer and let each developer decide how they want to extend it.
Below is my current favorite setup for Elm development in the tech stack described in my earlier post. It involves a few moving parts, so hold on!
Building the Base Container
The repository that builds the base container (tailored to my needs) is:
Note: The name is specific to my workflow, but feel free to get inspired.
The devcontainer Setup
Folder Structure
project-root/
├── .devcontainer/
│ ├── vanilla/ # Basic example
│ └── theodor/ # Personal environment config
│ ├── ssl/
│ │ ├── squidex.crt
│ │ └── squidex.key
│ ├── devcontainer.json
│ ├── docker-compose.yml
│ ├── Dockerfile
│ ├── Dockerfile.omnia
│ └── nginx.conf
├── backend/
├── backup/
├── build/
├── Documentation/
├── dotnet/
└── elm/
I’m considering adding theodor to .gitignore but haven’t decided yet.
Comments
- This setup is fairly advanced!
- Uses a custom domain locally – not
localhost:port. - Runs nginx in a production‑like configuration.
- Sets environment variables via nginx and the site‑config‑loader tool.
devcontainer.json
{
"name": "Dotnet 9/10 and Elm Dev Container (Debian + Compose)",
"dockerComposeFile": "docker-compose.yml",
"service": "dev",
"workspaceFolder": "/workspace",
"mounts": [
"source=ktk-elm-devcontainer,target=/home/container-user/.elm,type=volume"
],
"customizations": {
"vscode": {
"settings": {
"terminal.integrated.defaultProfile.linux": "zsh"
},
"extensions": [
"ms-vscode-remote.remote-containers",
"Elmtooling.elm-ls-vscode",
"ms-dotnettools.csharp",
"william-voyek.vscode-nginx",
"vscodevim.vim",
"ms-dotnettools.csdevkit",
"EditorConfig.EditorConfig",
"humao.rest-client",
"esbenp.prettier-vscode",
"DotJoshJohnson.xml",
"streetsidesoftware.code-spell-checker",
"streetsidesoftware.code-spell-checker-danish",
"bradlc.vscode-tailwindcss",
"kamikillerto.vscode-colorize",
"Ionide.Ionide-fsharp",
"ms-azuretools.vscode-containers",
"jebbs.plantuml",
"task.vscode-task",
"ecmel.vscode-html-css"
]
}
},
"remoteUser": "container-user",
"portsAttributes": {
"3033": { "label": "Elm" },
"5130": { "label": "Backend" },
"5140": { "label": "Gateway" },
"8314": { "label": "Nginx" },
"8376": { "label": "Squidex" },
"9876": { "label": "Elm" }
}
}
docker-compose.yml
services:
# Main development service
dev:
# Build the image for this service
build:
context: .
dockerfile: Dockerfile
# Volumes to mount
volumes:
- ../..:/workspace:cached
- ktk-elm-devcontainer:/home/container-user/.elm
# --- Alternative for Neovim config ---
# - ../.nvim:/root/.config/nvim:cached
# --- ADD THIS LINE ---
# - ./.config/nvim:/root/.config/nvim:cached
# command: ["sleep", "infinity"]
Keeping the Container Alive
command: sleep infinity
networks:
- internal
Services
nginx:
image: nginx:alpine
container_name: ktk_nginx
ports:
- "80:80"
- "443:443"
- "8314:80"
networks:
- internal
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
- dev
- squidex
mongo:
image: "mongo:6"
volumes:
- ktk_mongo_data:/data/db
networks:
- internal
restart: unless-stopped
squidex:
image: "squidex/squidex:7"
ports:
- "8376:5000"
environment:
- URLS__BASEURL=https://squidex.ktk.dk
- IDENTITY__ALLOWHTTPSCHEME=false
- EVENTSTORE__MONGODB__CONFIGURATION=mongodb://mongo
- STORE__MONGODB__CONFIGURATION=mongodb://mongo
- IDENTITY__ADMINEMAIL=sukkerfrit@gmail.com
- IDENTITY__ADMINPASSWORD=0hSoS3cret!
- ASPNETCORE_URLS=http://+:5000
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/healthz"]
start_period: 60s
depends_on:
- mongo
volumes:
- ktk_squidex_assets:/app/Assets
networks:
- internal
restart: unless-stopped
Volumes
volumes:
ktk-elm-devcontainer:
ktk_squidex_assets:
ktk_mongo_data:
Networks
networks:
internal:
driver: bridge
Dockerfile
FROM isuperman/elm-devcontainer-foundation:0.1.7
# Project specifics could be added here
Production‑like Local Development
This configuration lets you test routing, a tarpit, and more. A custom domain is used for local development, making the environment feel very close to production.
nginx.conf
worker_processes 1;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
resolver 127.0.0.11;
server {
listen 80;
server_name localhost ktk.dk;
location /api/ {
set $backend_upstream host.docker.internal:5130;
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://$backend_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
location /cms/ {
set $backend_upstream host.docker.internal:5140;
rewrite ^/cms/(.*)$ /$1 break;
proxy_pass http://$backend_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
location / {
set $frontend_upstream host.docker.internal:3033;
proxy_pass http://$frontend_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
sub_filter '' '';
sub_filter_once on;
}
location = /robots.txt {
set $frontend_upstream host.docker.internal:3033;
proxy_pass http://$frontend_upstream/robots.txt;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
location = /humans.txt {
set $frontend_upstream host.docker.internal:3033;
proxy_pass http://$frontend_upstream/humans.txt;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
}
server {
listen 80;
server_name squidex.ktk.dk;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name squidex.ktk.dk www.squidex.ktk.dk;
ssl_certificate /etc/nginx/ssl/squidex.crt;
ssl_certificate_key /etc/nginx/ssl/squidex.key;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://squidex:5000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_cookie_path / /;
}
}
}
Conclusion
This example shows how a devcontainer can be used to create a development environment that mirrors production. By using these settings in a local docker‑compose.yml, I’m extremely confident when pushing containers to production—bugs in production are now a rarity.
Next up we will focus on how to set up vite-plugin-elm-watch, which will…