initial devcontainer setup

This commit is contained in:
Jeff Clement 2025-07-18 23:12:35 +00:00
commit b9b7aa6a08
Signed by: jeff
GPG key ID: 3BCB43A3F0E1D7DA
14 changed files with 411 additions and 0 deletions

24
.devcontainer/Caddyfile Normal file
View file

@ -0,0 +1,24 @@
{
# Disable the Caddy admin API for security
admin off
}
# Main site block, listens on port 8001
:5001 {
# Custom error handling for 502 Bad Gateway
handle_errors 502 {
root * /data # Serve files from /data
rewrite * /offline.html # Rewrite all requests to offline.html
templates # Enable template processing
file_server # Serve static files
}
# Route for database web UI
route /dev/db/* {
uri strip_prefix /dev/db # Remove /dev/db prefix
reverse_proxy 127.0.0.1:8081 # Proxy to DB UI on port 8081
}
# Default route: proxy all other requests to main app
route /* {
reverse_proxy 127.0.0.1:4000 # Proxy to main app
}
}

21
.devcontainer/Dockerfile Normal file
View file

@ -0,0 +1,21 @@
FROM mcr.microsoft.com/devcontainers/base:jammy
# Get this thing up-to-date
RUN sudo sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' && \
curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc|sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/postgresql.gpg && \
apt-get update && \
apt-get upgrade -y && \
apt-get install -y git curl inotify-tools wget imagemagick sqlite3 libsqlite3-dev libgdiplus postgresql-16 build-essential && \
apt-get install -y build-essential autoconf m4 libncurses5-dev libwxgtk3.0-gtk3-dev libwxgtk-webview3.0-gtk3-dev libgl1-mesa-dev libglu1-mesa-dev libpng-dev libssh-dev unixodbc-dev xsltproc fop libxml2-utils libncurses-dev openjdk-11-jdk watchman
# Install MISE
RUN curl https://mise.run | MISE_INSTALL_PATH=/usr/local/bin/mise sh && \
echo 'eval "$(/usr/local/bin/mise activate bash)"' >> /home/vscode/.bashrc && \
echo 'eval "$(/usr/local/bin/mise activate bash --shims)"' >> /home/vscode/.bash_profile
# locale
ENV LANG en_US.UTF-8
ENV LC_ALL en_US.UTF-8
ENV LANGUAGE en_US:en
CMD ["/bin/bash"]

View file

@ -0,0 +1,61 @@
{
"name": "Ubuntu",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
"features": {
"ghcr.io/devcontainers/features/git:1": {}
},
"postCreateCommand": "bash .devcontainer/post_create.sh",
"forwardPorts": [
5001
],
"portsAttributes": {
"5001": {
"label": "PROXY"
}
},
"remoteEnv": {
"PROJECT_ROOT": "${containerWorkspaceFolder}",
"MISE_ENV": "dev,devcontainer"
},
"customizations": {
"vscode": {
"settings": {
"sqltools.connections": [
{
"name": "Development Database",
"driver": "PostgreSQL",
"previewLimit": 50,
"server": "postgres",
"port": 5432,
"database": "app",
"username": "postgres",
"password": "postgres"
}
]
},
"extensions": [
"github.codespaces",
"tamasfe.even-better-toml",
"phoenixframework.phoenix",
"JakeBecker.elixir-ls",
"mechatroner.rainbow-csv",
"mikestead.dotenv",
"bradlc.vscode-tailwindcss",
"github.vscode-github-actions",
"humao.rest-client",
"mtxr.sqltools-driver-pg",
"mtxr.sqltools"
]
}
}
}

View file

@ -0,0 +1,63 @@
version: '3'
# NOTE on `network_mode`
# All of the containers are bound together and share the same network context as the DB container.
# This means that each service needs to be listening to a unique port since they are all effectively
# mount to the same interface. i.e. Execute can connect to postgres on 127.0.0.1:5432 even though
# they are separate containers. Note that if, for some reason, Postgres fails to start, the other
# containers are going to have all manner of mysterious network connectivity issues.
services:
app:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
volumes:
- ..:/workspace:cached
depends_on:
- postgres
environment:
# Configure MISE to use DEV and DEVCONTAINER environments
MISE_ENV: dev,devcontainer
# Setup postgres environment variables
PGUSER: postgres
PGDATABASE: app
PGPASSWORD: postgres
PGHOST: postgres
# Overrides default command so things don't shut down after the process ends.
command: sleep infinity
network_mode: service:postgres
caddy:
image: caddy:latest
restart: unless-stopped
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- ./offline.html:/data/offline.html
network_mode: service:postgres
pgweb:
image: sosedoff/pgweb
restart: unless-stopped
environment:
- PGWEB_DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/app?sslmode=disable
- PGWEB_LOCK_SESSION=true # disable connect/disconnect buttons since they aren't useful here
depends_on:
- postgres # my database container is called postgres, not db
network_mode: service:postgres
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_DB: app
POSTGRES_PASSWORD: postgres
healthcheck:
test: pg_isready -U postgres -h 127.0.0.1
interval: 5s

13
.devcontainer/nuke_db.sh Executable file
View file

@ -0,0 +1,13 @@
#!/bin/bash
set -e
echo " - Clearing existing objects"
# Clean existing objects using schema drop
psql -d ${PGDATABASE} -v ON_ERROR_STOP=1 <<-EOSQL > /dev/null 2>&1
DROP SCHEMA PUBLIC CASCADE;
CREATE SCHEMA PUBLIC;
EOSQL
echo "Database nuked successfully"

View file

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Service Unavailable</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
padding: 50px;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
}
h1 {
font-size: 50px;
color: #333;
}
p {
font-size: 20px;
color: #666;
}
</style>
<meta http-equiv="refresh" content="2">
</head>
<body>
<div class="container">
<h1>Service Unavailable</h1>
<p>The service is currently unavailable.</p>
<p>This page will refresh every 2 seconds.</p>
</div>
</body>
</html>

View file

@ -0,0 +1,16 @@
# Change to the project root directory
cd $PROJECT_ROOT
# Trust all tools and plugins defined in mise.toml
mise trust --all
# Install all tools and plugins defined in mise.toml
mise install
# Activate mise environment for bash
eval "$(mise activate bash)"
# Run 'mix setup' only if mix.exs exists (i.e., if this is an Elixir project)
if [ -f mix.exs ]; then
mix setup
fi

79
.devcontainer/restore_db.sh Executable file
View file

@ -0,0 +1,79 @@
#!/bin/bash
set -e
safe_mode=false
# Parse command line arguments
while getopts ":s" opt; do
case $opt in
s)
safe_mode=true
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
esac
done
# Check if safe mode is enabled
if [ "$safe_mode" = true ]; then
# Count objects in public schema
object_count=$(psql -d ${PGDATABASE} -t -c "SELECT COUNT(*) FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = 'public';" | tr -d ' ')
if [ "$object_count" -gt 0 ]; then
echo "Safe mode: Public schema is not empty. Aborting import."
exit 1
fi
fi
# Shift the options so that $1 is the input path
shift $((OPTIND-1))
# If we were't given any arguments, make the user pick one!
if [ $# -eq 0 ]; then
INPUT_PATH=$(find $PROJECT_ROOT/.snapshots -type f -name "*.sql" | sort -r | fzf --no-sort --header="Select database to restore")
else
INPUT_PATH="$1"
fi
# Determine if input is directory or file
if [ -d "$INPUT_PATH" ]; then
# Find latest SQL file by name sorting (assuming timestamped filenames)
LATEST_FILE=$(ls "$INPUT_PATH"/*.sql 2>/dev/null | sort -r | head -n 1)
if [ -z "$LATEST_FILE" ]; then
echo "No .sql files found in directory"
exit 1
fi
DUMP_FILE="$LATEST_FILE"
elif [ -f "$INPUT_PATH" ]; then
DUMP_FILE="$INPUT_PATH"
else
echo "Invalid path: $INPUT_PATH"
exit 1
fi
# Validate the file exists
if [ ! -f "$DUMP_FILE" ]; then
echo "SQL file not found: $DUMP_FILE"
exit 1
fi
echo "Using dump file: $DUMP_FILE"
echo " - Clearing existing objects"
# Clean existing objects using schema drop
psql -d ${PGDATABASE} -v ON_ERROR_STOP=1 <<-EOSQL > /dev/null 2>&1
DROP SCHEMA PUBLIC CASCADE;
CREATE SCHEMA PUBLIC;
EOSQL
echo " - Importing database"
# Import new dump
psql -d ${PGDATABASE} -v ON_ERROR_STOP=1 -f "$DUMP_FILE" > /dev/null 2>&1
echo "Database imported successfully from: $DUMP_FILE"

33
.devcontainer/snap_db.sh Executable file
View file

@ -0,0 +1,33 @@
#!/bin/bash
# Ensure the .snapshots directory exists
mkdir -p "$PROJECT_ROOT/.snapshots"
# Generate the base filename with timestamp
base_filename="$PROJECT_ROOT/.snapshots/snapshot_$(date +%Y%m%d_%H%M%S)"
# Initialize an empty string for sanitized arguments
sanitized_args=""
# Loop through all arguments
for arg in "$@"; do
# Sanitize the argument: remove spaces and special characters
sanitized=$(echo "$arg" | tr -cd '[:alnum:]_-')
sanitized_args="${sanitized_args}_${sanitized}"
done
# Remove leading underscore if present
sanitized_args=${sanitized_args#_}
# Create the final filename
if [ -n "$sanitized_args" ]; then
filename="${base_filename}_${sanitized_args}.sql"
else
filename="${base_filename}.sql"
fi
# Run pg_dump and save to the generated filename
pg_dump --no-tablespaces ${PGDATABASE} -f "$filename"
# Print the filename that was written
echo "Snapshot saved to: $filename"