Add newproject command and fix template bugs
- Add newproject, ssh, templates commands for full project scaffolding - Add homelab SSH, Caddy, port, and proxy_host fields to Config - Fix dotEnvExample: use container-internal port (3000/8000/8080) not host port - Fix caddySnippet: use configurable proxy_host instead of localhost - Fix dockerComposeTemplate: add build: path so image-less deploys work - Commit gtea.exe binary
This commit is contained in:
@@ -24,6 +24,32 @@ var configCmd = &cobra.Command{
|
||||
cfg.Username = prompt(reader, fmt.Sprintf("Username [%s]", cfg.Username), cfg.Username)
|
||||
cfg.Token = prompt(reader, fmt.Sprintf("API Token [%s]", maskToken(cfg.Token)), cfg.Token)
|
||||
|
||||
fmt.Println("\n--- Homelab / beepc ---")
|
||||
cfg.SSHHost = prompt(reader, fmt.Sprintf("SSH host (e.g. beepc) [%s]", cfg.SSHHost), cfg.SSHHost)
|
||||
cfg.SSHUser = prompt(reader, fmt.Sprintf("SSH user [%s]", cfg.SSHUser), cfg.SSHUser)
|
||||
cfg.ProjectsDir = promptDefault(reader, fmt.Sprintf("Projects dir on host [%s]", cfg.ProjectsDir), cfg.ProjectsDir, "~/projects")
|
||||
cfg.HomelabDir = promptDefault(reader, fmt.Sprintf("Homelab repo dir on host [%s]", cfg.HomelabDir), cfg.HomelabDir, "~/homelab")
|
||||
proxyDefault := cfg.ProxyHost
|
||||
if proxyDefault == "" {
|
||||
proxyDefault = cfg.SSHHost
|
||||
}
|
||||
cfg.ProxyHost = promptDefault(reader, fmt.Sprintf("Proxy host for Caddy (IP/hostname VPS uses to reach beepc) [%s]", proxyDefault), cfg.ProxyHost, proxyDefault)
|
||||
|
||||
fmt.Println("\n--- Caddy ---")
|
||||
cfg.CaddyConfDir = promptDefault(reader, fmt.Sprintf("Caddy conf.d dir [%s]", cfg.CaddyConfDir), cfg.CaddyConfDir, "~/caddy/conf.d")
|
||||
cfg.CaddyReloadCmd = promptDefault(reader, fmt.Sprintf("Caddy reload command [%s]", cfg.CaddyReloadCmd), cfg.CaddyReloadCmd, "sudo caddy reload")
|
||||
cfg.Domain = promptDefault(reader, fmt.Sprintf("Base domain [%s]", cfg.Domain), cfg.Domain, "sketchferret.com")
|
||||
|
||||
fmt.Println("\n--- Port assignment ---")
|
||||
portStr := ""
|
||||
if cfg.PortStart != 0 {
|
||||
portStr = fmt.Sprintf("%d", cfg.PortStart)
|
||||
}
|
||||
portInput := promptDefault(reader, fmt.Sprintf("Port range start [%s]", portStr), portStr, "8200")
|
||||
if p, err := fmt.Sscanf(portInput, "%d", &cfg.PortStart); p == 0 || err != nil {
|
||||
cfg.PortStart = 8200
|
||||
}
|
||||
|
||||
if err := saveConfig(cfg); err != nil {
|
||||
return fmt.Errorf("saving config: %w", err)
|
||||
}
|
||||
@@ -42,6 +68,15 @@ func prompt(reader *bufio.Reader, label, fallback string) string {
|
||||
return input
|
||||
}
|
||||
|
||||
// promptDefault returns fallback if input is empty, otherwise defaultVal if fallback is also empty.
|
||||
func promptDefault(reader *bufio.Reader, label, fallback, defaultVal string) string {
|
||||
result := prompt(reader, label, fallback)
|
||||
if result == "" {
|
||||
return defaultVal
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func maskToken(t string) string {
|
||||
if len(t) < 4 {
|
||||
return "****"
|
||||
|
||||
217
cmd/newproject.go
Normal file
217
cmd/newproject.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
gogitea "code.gitea.io/sdk/gitea"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
npType string
|
||||
npPrivate bool
|
||||
)
|
||||
|
||||
var newprojectCmd = &cobra.Command{
|
||||
Use: "newproject <name>",
|
||||
Short: "Full project scaffold: local + Gitea + beepc clone + homelab service + Caddy subdomain",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runNewProject,
|
||||
}
|
||||
|
||||
func init() {
|
||||
newprojectCmd.Flags().StringVarP(&npType, "type", "t", "", "Stack type: node, python, go, static (required)")
|
||||
newprojectCmd.Flags().BoolVarP(&npPrivate, "private", "p", true, "Make the Gitea repo private")
|
||||
newprojectCmd.Flags().StringVarP(&repoDesc, "desc", "d", "", "Repo description")
|
||||
rootCmd.AddCommand(newprojectCmd)
|
||||
}
|
||||
|
||||
func runNewProject(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
|
||||
if npType == "" {
|
||||
return fmt.Errorf("--type/-t is required: node, python, go, static")
|
||||
}
|
||||
switch npType {
|
||||
case "node", "python", "go", "static":
|
||||
default:
|
||||
return fmt.Errorf("unknown type %q — valid: node, python, go, static", npType)
|
||||
}
|
||||
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.SSHHost == "" || cfg.SSHUser == "" {
|
||||
return fmt.Errorf("homelab SSH not configured — run 'gtea config' to set ssh_host and ssh_user")
|
||||
}
|
||||
|
||||
step := func(n int, msg string) { fmt.Printf("[%d/6] %s\n", n, msg) }
|
||||
fmt.Printf("==> Scaffolding %q (%s)\n\n", name, npType)
|
||||
|
||||
// 1. Local directory + git init
|
||||
step(1, "Creating local project directory...")
|
||||
if err := os.MkdirAll(name, 0755); err != nil {
|
||||
return fmt.Errorf("creating folder: %w", err)
|
||||
}
|
||||
if err := runGit(name, "init", "-b", "main"); err != nil {
|
||||
if err2 := runGit(name, "init"); err2 != nil {
|
||||
return err2
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Starter files
|
||||
step(2, "Writing starter files...")
|
||||
if err := writeStarterFiles(name, npType); err != nil {
|
||||
return fmt.Errorf("writing starter files: %w", err)
|
||||
}
|
||||
|
||||
// 3. Gitea repo
|
||||
step(3, "Creating Gitea repo...")
|
||||
client, err := gogitea.NewClient(cfg.URL, gogitea.SetToken(cfg.Token))
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to Gitea: %w", err)
|
||||
}
|
||||
repo, _, err := client.CreateRepo(gogitea.CreateRepoOption{
|
||||
Name: name,
|
||||
Description: repoDesc,
|
||||
Private: npPrivate,
|
||||
AutoInit: false,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating Gitea repo: %w", err)
|
||||
}
|
||||
fmt.Println(" →", repo.HTMLURL)
|
||||
|
||||
// 4. Initial commit and push
|
||||
step(4, "Initial commit and push...")
|
||||
remote := authCloneURL(cfg, repo.CloneURL)
|
||||
if err := runGit(name, "remote", "add", "origin", remote); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := runGit(name, "add", "."); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := runGit(name, "commit", "-m", "Initial commit"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := runGit(name, "push", "-u", "origin", "main"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Resolve config defaults
|
||||
projectsDir := orDefault(cfg.ProjectsDir, "~/projects")
|
||||
homelabDir := orDefault(cfg.HomelabDir, "~/homelab")
|
||||
domain := orDefault(cfg.Domain, "sketchferret.com")
|
||||
caddyDir := orDefault(cfg.CaddyConfDir, "~/caddy/conf.d")
|
||||
caddyReload := orDefault(cfg.CaddyReloadCmd, "sudo caddy reload")
|
||||
proxyHost := orDefault(cfg.ProxyHost, cfg.SSHHost)
|
||||
portStart := cfg.PortStart
|
||||
if portStart == 0 {
|
||||
portStart = 8200
|
||||
}
|
||||
|
||||
// 5. beepc: clone + homelab service
|
||||
step(5, fmt.Sprintf("Setting up beepc (%s@%s)...", cfg.SSHUser, cfg.SSHHost))
|
||||
|
||||
// Auto-assign port by counting existing service dirs
|
||||
out, _ := sshRun(cfg.SSHUser, cfg.SSHHost,
|
||||
fmt.Sprintf("ls %s/services/ 2>/dev/null | wc -l", homelabDir))
|
||||
count, _ := strconv.Atoi(strings.TrimSpace(out))
|
||||
port := portStart + count
|
||||
|
||||
// Clone to ~/projects/<name>
|
||||
remotePath := projectsDir + "/" + name
|
||||
cloneURL := authCloneURL(cfg, repo.CloneURL)
|
||||
if _, err := sshRun(cfg.SSHUser, cfg.SSHHost,
|
||||
fmt.Sprintf("mkdir -p %s && git clone %s %s", projectsDir, cloneURL, remotePath)); err != nil {
|
||||
return fmt.Errorf("cloning on beepc: %w", err)
|
||||
}
|
||||
fmt.Printf(" Cloned → %s\n", remotePath)
|
||||
|
||||
// Homelab service
|
||||
serviceDir := homelabDir + "/services/" + name
|
||||
if err := sshWriteFile(cfg.SSHUser, cfg.SSHHost,
|
||||
serviceDir+"/docker-compose.yml",
|
||||
dockerComposeTemplate(name, npType, port)); err != nil {
|
||||
return fmt.Errorf("writing docker-compose.yml: %w", err)
|
||||
}
|
||||
if err := sshWriteFile(cfg.SSHUser, cfg.SSHHost,
|
||||
serviceDir+"/.env.example",
|
||||
dotEnvExample(name, npType, port)); err != nil {
|
||||
return fmt.Errorf("writing .env.example: %w", err)
|
||||
}
|
||||
fmt.Printf(" Homelab service → %s\n", serviceDir)
|
||||
|
||||
// 6. Caddy
|
||||
step(6, "Adding Caddy config...")
|
||||
subdomain := name + "." + domain
|
||||
if err := sshWriteFile(cfg.SSHUser, cfg.SSHHost,
|
||||
caddyDir+"/"+name+".caddy",
|
||||
caddySnippet(subdomain, proxyHost, port)); err != nil {
|
||||
return fmt.Errorf("writing Caddy snippet: %w", err)
|
||||
}
|
||||
if _, err := sshRun(cfg.SSHUser, cfg.SSHHost, caddyReload); err != nil {
|
||||
fmt.Printf(" Warning: Caddy reload failed — reload manually when ready\n")
|
||||
} else {
|
||||
fmt.Printf(" https://%s → localhost:%d\n", subdomain, port)
|
||||
}
|
||||
|
||||
fmt.Printf(`
|
||||
Done!
|
||||
Local: ./%s
|
||||
Gitea: %s
|
||||
beepc: %s
|
||||
Service: %s
|
||||
URL: https://%s
|
||||
Port: %d
|
||||
`, name, repo.HTMLURL, remotePath, serviceDir, subdomain, port)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeStarterFiles(dir, stackType string) error {
|
||||
files := map[string]string{
|
||||
".gitignore": gitignoreFor(stackType),
|
||||
"README.md": "# " + dir + "\n",
|
||||
}
|
||||
switch stackType {
|
||||
case "node":
|
||||
files["package.json"] = nodePackageJSON(dir)
|
||||
files["tsconfig.json"] = tsconfigJSON()
|
||||
files["src/index.ts"] = nodeIndexTS()
|
||||
files["Dockerfile"] = nodeDockerfile()
|
||||
case "python":
|
||||
files["requirements.txt"] = "# Add dependencies here\n"
|
||||
files["app.py"] = pythonApp()
|
||||
files["Dockerfile"] = pythonDockerfile()
|
||||
case "go":
|
||||
files["go.mod"] = goMod(dir)
|
||||
files["main.go"] = goMain()
|
||||
files["Dockerfile"] = goDockerfile()
|
||||
case "static":
|
||||
files["index.html"] = staticHTML(dir)
|
||||
files["Dockerfile"] = staticDockerfile()
|
||||
}
|
||||
for relPath, content := range files {
|
||||
absPath := filepath.Join(dir, relPath)
|
||||
if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(absPath, []byte(content), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func orDefault(s, def string) string {
|
||||
if s != "" {
|
||||
return s
|
||||
}
|
||||
return def
|
||||
}
|
||||
15
cmd/root.go
15
cmd/root.go
@@ -17,6 +17,21 @@ type Config struct {
|
||||
URL string `json:"url"`
|
||||
Token string `json:"token"`
|
||||
Username string `json:"username"`
|
||||
|
||||
// Homelab / beepc
|
||||
SSHHost string `json:"ssh_host"`
|
||||
SSHUser string `json:"ssh_user"`
|
||||
ProjectsDir string `json:"projects_dir"`
|
||||
HomelabDir string `json:"homelab_dir"`
|
||||
ProxyHost string `json:"proxy_host"` // IP/hostname Caddy on VPS uses to reach beepc
|
||||
|
||||
// Caddy reverse proxy
|
||||
CaddyConfDir string `json:"caddy_conf_dir"`
|
||||
CaddyReloadCmd string `json:"caddy_reload_cmd"`
|
||||
|
||||
// Project defaults
|
||||
Domain string `json:"domain"`
|
||||
PortStart int `json:"port_range_start"`
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
|
||||
37
cmd/ssh.go
Normal file
37
cmd/ssh.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// sshRun executes a shell command on the remote host, returning combined output.
|
||||
// Uses the system ssh binary so existing keys and ~/.ssh/config are honored.
|
||||
func sshRun(user, host, command string) (string, error) {
|
||||
c := exec.Command("ssh", "-o", "BatchMode=yes", user+"@"+host, command)
|
||||
out, err := c.CombinedOutput()
|
||||
if err != nil {
|
||||
trimmed := strings.TrimSpace(string(out))
|
||||
if trimmed != "" {
|
||||
return string(out), fmt.Errorf("ssh %s@%s: %s: %w", user, host, trimmed, err)
|
||||
}
|
||||
return string(out), fmt.Errorf("ssh %s@%s: command failed: %w", user, host, err)
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
// sshWriteFile writes content to a remote file, creating parent directories.
|
||||
// Content is streamed via stdin to avoid shell escaping issues.
|
||||
func sshWriteFile(user, host, remotePath, content string) error {
|
||||
dir := path.Dir(remotePath)
|
||||
cmd := fmt.Sprintf("mkdir -p %s && cat > %s", dir, remotePath)
|
||||
c := exec.Command("ssh", "-o", "BatchMode=yes", user+"@"+host, cmd)
|
||||
c.Stdin = strings.NewReader(content)
|
||||
if out, err := c.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("writing %s on %s@%s: %s: %w",
|
||||
remotePath, user, host, strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
226
cmd/templates.go
Normal file
226
cmd/templates.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package cmd
|
||||
|
||||
import "fmt"
|
||||
|
||||
func gitignoreFor(stackType string) string {
|
||||
base := ".env\n.env.*\n!.env.example\n"
|
||||
switch stackType {
|
||||
case "node":
|
||||
return base + "node_modules/\ndist/\n.next/\n*.log\n"
|
||||
case "python":
|
||||
return base + "__pycache__/\n*.pyc\n.venv/\nvenv/\n*.egg-info/\ndist/\nbuild/\n"
|
||||
case "go":
|
||||
return base + "*.exe\n*.test\n*.out\n/vendor/\n"
|
||||
case "static":
|
||||
return base + "dist/\n.cache/\n"
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func dockerComposeTemplate(name, stackType string, port int) string {
|
||||
projectsDir := "~/projects"
|
||||
var svc string
|
||||
switch stackType {
|
||||
case "node":
|
||||
svc = fmt.Sprintf(
|
||||
" app:\n image: ${IMAGE:-ghcr.io/sketchferret/%s:latest}\n"+
|
||||
" build: %s/%s\n"+
|
||||
" restart: unless-stopped\n ports:\n - \"%d:3000\"\n env_file: .env\n",
|
||||
name, projectsDir, name, port)
|
||||
case "python":
|
||||
svc = fmt.Sprintf(
|
||||
" app:\n image: ${IMAGE:-ghcr.io/sketchferret/%s:latest}\n"+
|
||||
" build: %s/%s\n"+
|
||||
" restart: unless-stopped\n ports:\n - \"%d:8000\"\n env_file: .env\n",
|
||||
name, projectsDir, name, port)
|
||||
case "go":
|
||||
svc = fmt.Sprintf(
|
||||
" app:\n image: ${IMAGE:-ghcr.io/sketchferret/%s:latest}\n"+
|
||||
" build: %s/%s\n"+
|
||||
" restart: unless-stopped\n ports:\n - \"%d:8080\"\n env_file: .env\n",
|
||||
name, projectsDir, name, port)
|
||||
case "static":
|
||||
svc = fmt.Sprintf(
|
||||
" web:\n image: nginx:alpine\n restart: unless-stopped\n"+
|
||||
" ports:\n - \"%d:80\"\n volumes:\n - ./dist:/usr/share/nginx/html:ro\n",
|
||||
port)
|
||||
}
|
||||
return "services:\n" + svc
|
||||
}
|
||||
|
||||
func dotEnvExample(name, stackType string, port int) string {
|
||||
_ = port // host port is set by compose; container uses its own internal port
|
||||
header := fmt.Sprintf("# %s environment\n# Copy to .env and fill in values\n\n", name)
|
||||
switch stackType {
|
||||
case "node":
|
||||
return header + "PORT=3000\n# DATABASE_URL=\n# SECRET_KEY=\n"
|
||||
case "python":
|
||||
return header + "PORT=8000\n# DATABASE_URL=\n# SECRET_KEY=\n"
|
||||
case "go":
|
||||
return header + "PORT=8080\n# DATABASE_URL=\n"
|
||||
}
|
||||
return header
|
||||
}
|
||||
|
||||
func caddySnippet(subdomain, proxyHost string, port int) string {
|
||||
return fmt.Sprintf("%s {\n\treverse_proxy %s:%d\n}\n", subdomain, proxyHost, port)
|
||||
}
|
||||
|
||||
// --- local starter files ---
|
||||
|
||||
func nodePackageJSON(name string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"name": "%s",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"dev": "ts-node src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0",
|
||||
"ts-node": "^10.0.0",
|
||||
"@types/node": "^20.0.0"
|
||||
}
|
||||
}
|
||||
`, name)
|
||||
}
|
||||
|
||||
func tsconfigJSON() string {
|
||||
return `{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func nodeIndexTS() string {
|
||||
// Template literal backtick handled via concatenation
|
||||
return "import http from 'http';\n\n" +
|
||||
"const port = process.env.PORT ? parseInt(process.env.PORT) : 3000;\n\n" +
|
||||
"const server = http.createServer((_req, res) => {\n" +
|
||||
" res.writeHead(200, { 'Content-Type': 'text/plain' });\n" +
|
||||
" res.end('Hello World\\n');\n" +
|
||||
"});\n\n" +
|
||||
"server.listen(port, () => {\n" +
|
||||
" console.log(`Server running on port ${port}`);\n" +
|
||||
"});\n"
|
||||
}
|
||||
|
||||
func nodeDockerfile() string {
|
||||
return `FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
EXPOSE 3000
|
||||
CMD ["node", "dist/index.js"]
|
||||
`
|
||||
}
|
||||
|
||||
func pythonApp() string {
|
||||
return `from flask import Flask
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return "Hello World"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
`
|
||||
}
|
||||
|
||||
func pythonDockerfile() string {
|
||||
return `FROM python:3.12-slim
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY . .
|
||||
EXPOSE 8000
|
||||
CMD ["python", "app.py"]
|
||||
`
|
||||
}
|
||||
|
||||
func goMod(name string) string {
|
||||
return fmt.Sprintf("module %s\n\ngo 1.22\n", name)
|
||||
}
|
||||
|
||||
func goMain() string {
|
||||
return `package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, "Hello World")
|
||||
})
|
||||
fmt.Println("Listening on :" + port)
|
||||
if err := http.ListenAndServe(":"+port, nil); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func goDockerfile() string {
|
||||
return `FROM golang:1.22-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY go.* ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -o app .
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /app/app /app
|
||||
EXPOSE 8080
|
||||
CMD ["/app"]
|
||||
`
|
||||
}
|
||||
|
||||
func staticHTML(name string) string {
|
||||
return fmt.Sprintf(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>%s</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>%s</h1>
|
||||
</body>
|
||||
</html>
|
||||
`, name, name)
|
||||
}
|
||||
|
||||
func staticDockerfile() string {
|
||||
return `FROM nginx:alpine
|
||||
COPY . /usr/share/nginx/html
|
||||
EXPOSE 80
|
||||
`
|
||||
}
|
||||
Reference in New Issue
Block a user