Serve HTML forms from a JSON config file. No code changes needed to deploy a new form.
544
A Docker-containerized web server that renders fully functional HTML forms from a JSON configuration file. Deploy any form by editing two files — no code changes needed.
.json file/config page with a JSON text editor and point-and-click form buildergit clone https://github.com/TephlonDude/web-form-server.git
cd web-form-server
docker compose up -d
Open http://localhost:8237 to see the demo form. Open http://localhost:8237/config to edit it.
| Layer | Technology |
|---|---|
| Language | Go 1.22 — stdlib only (net/http, html/template, encoding/json, embed) |
| Container | Docker + Docker Compose — multi-stage build, ~15 MB Alpine image |
| Config editor | CodeMirror 5 (v5.65.16, cdnjs) — syntax highlighting, line numbers, code folding |
| Client-side | Vanilla JavaScript — Visual Builder, multi-page navigation |
| Form validation | HTML5 checkValidity() API — per-page validation in multi-step forms |
The form is defined in a JSON file (default: web-config/example.form.json). Point to your own file with the FORM_FILE environment variable.
Use one of two layouts:
| Layout | When to use | Top-level key |
|---|---|---|
| Single-page | One scrollable page | "sections" |
| Multi-page | Wizard with steps | "pages" |
{
"form": {
"title": "Contact Us",
"submit_label": "Send Message",
"sections": [
{
"id": "contact",
"title": "Your Details",
"fields": [
{
"id": "full_name",
"type": "text",
"label": "Full Name",
"required": true
}
]
}
]
}
}
{
"form": {
"title": "Registration",
"submit_label": "Complete Registration",
"pages": [
{
"id": "step1",
"title": "Personal Info",
"description": "Tell us about yourself.",
"sections": [
{
"id": "basics",
"title": "Basic Details",
"fields": [
{
"id": "full_name",
"type": "text",
"label": "Full Name"
}
]
}
]
},
{
"id": "step2",
"title": "Preferences",
"sections": [
{
"id": "prefs",
"title": "Your Preferences",
"fields": [
{
"id": "newsletter",
"type": "checkbox",
"label": "Subscribe to newsletter",
"value": "yes"
}
]
}
]
}
]
}
}
| Key | Required | Description |
|---|---|---|
title | Yes | Page <title> and form heading |
description | Subtitle shown below the heading | |
action | Form action URL (default: /submit) | |
method | post or get (default: post) | |
enctype | Set to multipart/form-data when using file fields | |
submit_label | Submit button text (default: Submit) | |
reset_label | Reset button text; omit to hide the button |
| Key | Required | Description |
|---|---|---|
id | Yes | Unique identifier, used as HTML id |
title | Section heading rendered as <legend> | |
description | Optional paragraph below the heading |
Every field type supports these keys:
| Key | Type | Description |
|---|---|---|
id | string | Required. HTML name and id attribute |
type | string | Required. See field types below |
label | string | Required (except hidden). Visible label text |
required | bool | Browser validation — blocks submission if empty |
disabled | bool | Greys out the field; value not submitted |
readonly | bool | Prevents editing; value is submitted |
value | string | Pre-populated default value |
placeholder | string | Ghost text shown when empty |
autocomplete | string | HTML autocomplete attribute (e.g. name, email, off) |
title | string | Tooltip shown on hover |
css_class | string | Extra CSS class added to the field wrapper |
text password email url tel search
{
"id": "full_name",
"type": "text",
"label": "Full Name",
"required": true,
"placeholder": "Jane Doe",
"pattern": "[A-Za-z ]+",
"minlength": 2,
"maxlength": 100,
"autocomplete": "name"
}
| Extra key | Description |
|---|---|
pattern | Regex the value must match |
minlength | Minimum character count |
maxlength | Maximum character count |
number range
{
"id": "age",
"type": "number",
"label": "Age",
"min": 0,
"max": 120,
"step": 1
}
{
"id": "rating",
"type": "range",
"label": "Satisfaction (1–10)",
"min": 1,
"max": 10,
"step": 1,
"value": "5"
}
Range fields render a live <output> display alongside the slider.
| Extra key | Description |
|---|---|
min | Minimum value |
max | Maximum value |
step | Increment size |
date time datetime-local month week
{
"id": "birth_date",
"type": "date",
"label": "Date of Birth",
"min": "1900-01-01",
"max": "2010-12-31"
}
{
"id": "appt_time",
"type": "time",
"label": "Appointment Time",
"min": "08:00",
"max": "17:00",
"step": 900
}
{
"id": "meeting",
"type": "datetime-local",
"label": "Meeting Date & Time",
"min": "2026-01-01T08:00",
"max": "2027-12-31T17:00"
}
min and max accept the format matching the field type:
date → YYYY-MM-DDtime → HH:MMdatetime-local → YYYY-MM-DDTHH:MMmonth → YYYY-MMweek → YYYY-Wwwstep for time is in seconds (e.g. 900 = 15-minute intervals).
{
"id": "brand_color",
"type": "color",
"label": "Brand Color",
"value": "#3498db"
}
{
"id": "comments",
"type": "textarea",
"label": "Additional Comments",
"placeholder": "Enter your thoughts here…",
"rows": 6,
"cols": 60,
"maxlength": 2000
}
| Extra key | Description |
|---|---|
rows | Visible row count |
cols | Visible column count |
maxlength | Maximum character count |
{
"id": "country",
"type": "select",
"label": "Country",
"required": true,
"placeholder": "-- Select a country --",
"options": [
{ "label": "United States", "value": "us" },
{ "label": "Canada", "value": "ca", "selected": true },
{ "label": "Other", "value": "other" }
]
}
{
"id": "languages",
"type": "select",
"label": "Languages Spoken",
"multiple": true,
"size": 5,
"options": [
{ "label": "English", "value": "en", "selected": true },
{ "label": "French", "value": "fr" },
{ "label": "Spanish", "value": "es" }
]
}
| Extra key | Description |
|---|---|
placeholder | Disabled first option (single select only) |
multiple | Allow multiple selections; submits as a list |
size | Number of visible options (multi-select) |
Option keys: label (required), value (required), selected, disabled
{
"id": "plan",
"type": "radio",
"label": "Choose Plan",
"required": true,
"options": [
{ "label": "Free", "value": "free" },
{ "label": "Pro", "value": "pro", "checked": true },
{ "label": "Enterprise", "value": "enterprise", "disabled": true }
]
}
Option keys: label (required), value (required), checked, disabled
{
"id": "agree_terms",
"type": "checkbox",
"label": "I agree to the Terms of Service",
"required": true,
"value": "yes"
}
value is the submitted value when checked (default: "on").
Submits as fieldname[] so the server receives a list.
{
"id": "interests",
"type": "checkbox_group",
"label": "Interests",
"options": [
{ "label": "Technology", "value": "tech", "checked": true },
{ "label": "Sports", "value": "sports" },
{ "label": "Music", "value": "music", "disabled": true }
]
}
{
"id": "resume",
"type": "file",
"label": "Upload Resume",
"accept": ".pdf,.doc,.docx"
}
{
"id": "photos",
"type": "file",
"label": "Upload Photos",
"accept": "image/*",
"multiple": true
}
When any file field is present, set "enctype": "multipart/form-data" on the form.
{
"id": "form_version",
"type": "hidden",
"value": "2.0.0"
}
{
"id": "preview_btn",
"type": "button",
"value": "Preview",
"css_class": "btn-secondary",
"title": "Preview before submitting"
}
Navigate to /config while the container is running.
JSON Editor tab — Edit the raw JSON file directly with Load and Save buttons. The save endpoint validates the JSON before writing; invalid configs are rejected with an error message.
Visual Builder tab — Point-and-click form construction:
select, radio, and checkbox_group fieldsEvery submission is:
Logged to stdout as a JSON line:
{"timestamp":"2026-03-12T14:30:00Z","form_title":"Contact Us","fields":{"full_name":"Jane Doe","email":"[email protected]"},"files":[]}
Optionally saved to a JSON file when SAVE_SUBMISSIONS=true:
data/submissions/20260312_143000_000000.json
Files are named with a UTC timestamp (sorts chronologically). The directory is volume-mounted so files persist across container restarts.
The CSS file (default: web-config/example.form.css) is loaded at startup and injected inline — one HTTP request returns a fully self-contained page.
Key CSS hook points:
/* Page wrapper */
body, .form-container {}
/* Section grouping */
fieldset, legend {}
/* Per-field wrapper (all types) */
.field-group, .field-group label {}
/* Input elements */
input[type="text"], input[type="email"], textarea, select {}
/* Choice groups */
.radio-group .radio-option {}
.checkbox-group .checkbox-option {}
.checkbox-single {}
/* Range slider + live value */
input[type="range"], output {}
/* Required asterisk */
.required {}
/* Submit area */
.form-actions {}
button[type="submit"], button[type="reset"] {}
/* Browser validation states */
input:invalid, input:valid {}
/* Multi-page navigation */
.page-steps, .step-item, .step-bubble, .step-label {}
.progress-bar, .progress-fill {}
.page-actions, .btn-next, .btn-prev {}
| Variable | Default | Description |
|---|---|---|
FORM_FILE | /web-config/form.json | Path to the JSON form definition |
CSS_FILE | /web-config/form.css | Path to the CSS stylesheet |
PORT | 5000 | Container listen port |
SAVE_SUBMISSIONS | false | Write JSON files to SUBMISSIONS_DIR when true |
SUBMISSIONS_DIR | /data/submissions | Directory for saved submission JSON files |
CONFIG_USER | (unset) | Username to protect /config with HTTP Basic Auth |
CONFIG_PASS | (unset) | Password to protect /config with HTTP Basic Auth |
Security: Always set
CONFIG_USERandCONFIG_PASSon any internet-facing deployment to protect the form editor.
Override any variable in docker-compose.yml:
environment:
FORM_FILE: /web-config/my-form.json
CSS_FILE: /web-config/my-form.css
SAVE_SUBMISSIONS: "true"
docker compose up -d --build # Build and start
docker compose logs -f # Follow logs
docker compose down # Stop
The host port defaults to 8237. Override with HOST_PORT:
HOST_PORT=9000 docker compose up -d
./deploy.sh # Build + transfer image + restart on server
./deploy.sh --rebuild # Force full rebuild (no cache)
The script builds the image locally, saves it as a tarball, copies it via SCP, loads it on the remote host, and restarts the container. Requires Docker on the remote host and SSH key authentication. Copy .server-config.example to .server-config and fill in your server details before running.
Web Form Server/
├── main.go # HTTP server, route registration
├── form_loader.go # JSON parsing, validation, struct definitions
├── renderer.go # HTML rendering for all field types
├── submission.go # Submission logging and JSON file output
├── config_handler.go # /config route handlers
├── go.mod # Go module definition
├── Dockerfile # Multi-stage build (builder + alpine runtime)
├── docker-compose.yml # Service definition with volume mounts
├── deploy.sh # Build-and-deploy script for remote server
├── CLAUDE.md # Claude Code project context
├── templates/
│ ├── form.html # Form rendering template (multi + single page)
│ ├── submitted.html # Post-submission confirmation page
│ └── config.html # Visual config editor page
├── web-config/
│ ├── example.form.json # Demo form covering all field types
│ └── example.form.css # Default stylesheet
└── data/
└── submissions/ # Saved submission JSON files (volume-mounted)
Requires Go 1.22+.
go mod tidy # Fetch dependencies
go build -o web-form-server . # Build binary
go vet ./... # Static analysis
./web-form-server # Run (set FORM_FILE and CSS_FILE env vars)
Templates are embedded into the binary at compile time via //go:embed. Changes to template files require a rebuild.
Config and CSS files are read from disk at request time — edit them and refresh the browser with no rebuild needed.
Content type
Image
Digest
sha256:b42aea38a…
Size
8.2 MB
Last updated
6 months ago
docker pull tephlon/web-form-server