@@ -4,8 +4,11 @@ import (
44 "context"
55 "fmt"
66 "lightfold/pkg/builders"
7+ "lightfold/pkg/config"
78 "os"
9+ "os/exec"
810 "path/filepath"
11+ "strings"
912)
1013
1114// DockerfileBuilder uses an existing Dockerfile for building
@@ -22,32 +25,222 @@ func (d *DockerfileBuilder) Name() string {
2225}
2326
2427func (d * DockerfileBuilder ) IsAvailable () bool {
25- // TODO: Implement Dockerfile builder - check for Docker daemon availability
26- // Dockerfile builder is not yet implemented
27- return false
28+ // Check if docker command exists
29+ _ , err := exec .LookPath ("docker" )
30+ if err != nil {
31+ return false
32+ }
33+
34+ // Check if Docker daemon is accessible
35+ cmd := exec .Command ("docker" , "info" )
36+ if err := cmd .Run (); err != nil {
37+ return false
38+ }
39+
40+ return true
2841}
2942
3043func (d * DockerfileBuilder ) NeedsNginx () bool {
44+ // Assume Dockerfile includes its own web server
45+ // User can configure nginx separately if needed
3146 return false
3247}
3348
3449func (d * DockerfileBuilder ) Build (ctx context.Context , opts * builders.BuildOptions ) (* builders.BuildResult , error ) {
50+ // Verify Dockerfile exists
3551 dockerfilePath := filepath .Join (opts .ProjectPath , "Dockerfile" )
3652 if _ , err := os .Stat (dockerfilePath ); os .IsNotExist (err ) {
3753 return & builders.BuildResult {
3854 Success : false ,
3955 }, fmt .Errorf ("Dockerfile not found at %s" , dockerfilePath )
4056 }
4157
42- // TODO: Implement dockerfile builder
43- // - Build image: docker build -t <app>:latest .
44- // - Export as tarball: docker save <app>:latest -o /tmp/image.tar
45- // - Transfer to server via SSH
46- // - Load and run: docker load < image.tar && docker run ...
58+ // Extract app name from release path
59+ appName := extractAppName (opts .ReleasePath )
60+ if appName == "" {
61+ return & builders.BuildResult {
62+ Success : false ,
63+ }, fmt .Errorf ("failed to extract app name from release path: %s" , opts .ReleasePath )
64+ }
65+
66+ imageName := fmt .Sprintf ("lightfold-%s:latest" , appName )
67+ tarballPath := filepath .Join (os .TempDir (), fmt .Sprintf ("lightfold-%s-image.tar" , appName ))
68+
69+ var buildLog strings.Builder
70+
71+ // Step 1: Build Docker image locally
72+ buildLog .WriteString (fmt .Sprintf ("Building Docker image: %s\n " , imageName ))
73+ buildCmd := exec .CommandContext (ctx , "docker" , "build" , "-t" , imageName , opts .ProjectPath )
74+ buildCmd .Dir = opts .ProjectPath
75+
76+ buildOutput , err := buildCmd .CombinedOutput ()
77+ buildLog .Write (buildOutput )
78+
79+ if err != nil {
80+ return & builders.BuildResult {
81+ Success : false ,
82+ BuildLog : buildLog .String (),
83+ }, fmt .Errorf ("docker build failed: %w\n Output: %s" , err , buildOutput )
84+ }
85+
86+ // Step 2: Export image as tarball
87+ buildLog .WriteString (fmt .Sprintf ("\n Exporting image to tarball: %s\n " , tarballPath ))
88+ saveCmd := exec .CommandContext (ctx , "docker" , "save" , "-o" , tarballPath , imageName )
89+
90+ saveOutput , err := saveCmd .CombinedOutput ()
91+ buildLog .Write (saveOutput )
92+
93+ if err != nil {
94+ return & builders.BuildResult {
95+ Success : false ,
96+ BuildLog : buildLog .String (),
97+ }, fmt .Errorf ("docker save failed: %w\n Output: %s" , err , saveOutput )
98+ }
99+
100+ // Ensure cleanup of tarball
101+ defer os .Remove (tarballPath )
102+
103+ // Step 3: Ensure Docker is installed on remote server
104+ buildLog .WriteString ("\n Checking Docker on remote server...\n " )
105+ ssh := opts .SSHExecutor
106+
107+ dockerCheck := ssh .Execute ("which docker" )
108+ if dockerCheck .ExitCode != 0 {
109+ buildLog .WriteString ("Installing Docker on remote server...\n " )
110+
111+ // Install Docker using official script
112+ installCmds := []string {
113+ "curl -fsSL https://get.docker.com -o /tmp/get-docker.sh" ,
114+ "sudo sh /tmp/get-docker.sh" ,
115+ "sudo usermod -aG docker deploy" ,
116+ "rm /tmp/get-docker.sh" ,
117+ }
118+
119+ for _ , cmd := range installCmds {
120+ result := ssh .ExecuteSudo (cmd )
121+ buildLog .WriteString (result .Stdout )
122+ buildLog .WriteString (result .Stderr )
123+
124+ if result .ExitCode != 0 {
125+ return & builders.BuildResult {
126+ Success : false ,
127+ BuildLog : buildLog .String (),
128+ }, fmt .Errorf ("failed to install Docker on remote server: %s" , result .Stderr )
129+ }
130+ }
131+
132+ buildLog .WriteString ("Docker installed successfully\n " )
133+ }
134+
135+ // Step 4: Transfer tarball to remote server
136+ remoteTarballPath := fmt .Sprintf ("/tmp/lightfold-%s-image.tar" , appName )
137+ buildLog .WriteString (fmt .Sprintf ("\n Transferring image to server: %s\n " , remoteTarballPath ))
138+
139+ if err := ssh .UploadFile (tarballPath , remoteTarballPath ); err != nil {
140+ return & builders.BuildResult {
141+ Success : false ,
142+ BuildLog : buildLog .String (),
143+ }, fmt .Errorf ("failed to transfer image tarball: %w" , err )
144+ }
145+
146+ // Step 5: Load Docker image on remote server
147+ buildLog .WriteString ("\n Loading Docker image on remote server...\n " )
148+ loadResult := ssh .Execute (fmt .Sprintf ("docker load -i %s" , remoteTarballPath ))
149+ buildLog .WriteString (loadResult .Stdout )
150+ buildLog .WriteString (loadResult .Stderr )
151+
152+ if loadResult .ExitCode != 0 {
153+ ssh .Execute (fmt .Sprintf ("rm %s" , remoteTarballPath ))
154+ return & builders.BuildResult {
155+ Success : false ,
156+ BuildLog : buildLog .String (),
157+ }, fmt .Errorf ("docker load failed: %s" , loadResult .Stderr )
158+ }
159+
160+ // Cleanup remote tarball
161+ ssh .Execute (fmt .Sprintf ("rm %s" , remoteTarballPath ))
162+
163+ // Step 6: Write environment variables to .env file if provided
164+ if len (opts .EnvVars ) > 0 {
165+ var envContent strings.Builder
166+ for key , value := range opts .EnvVars {
167+ envContent .WriteString (fmt .Sprintf ("%s=%s\n " , key , value ))
168+ }
169+
170+ envPath := fmt .Sprintf ("%s/.env" , opts .ReleasePath )
171+ if err := ssh .WriteRemoteFile (envPath , envContent .String (), config .PermEnvFile ); err != nil {
172+ return & builders.BuildResult {
173+ Success : false ,
174+ BuildLog : buildLog .String (),
175+ }, fmt .Errorf ("failed to write .env file: %w" , err )
176+ }
177+
178+ ssh .ExecuteSudo (fmt .Sprintf ("chown deploy:deploy %s" , envPath ))
179+ buildLog .WriteString (fmt .Sprintf ("\n Wrote environment variables to %s\n " , envPath ))
180+ }
181+
182+ // Step 7: Create docker-compose.yml or docker run script
183+ // We'll create a simple run script that can be used by systemd
184+ port := extractPortFromEnv (opts .EnvVars )
185+ if port == "" {
186+ port = "3000" // Default port
187+ }
188+
189+ runScript := fmt .Sprintf (`#!/bin/bash
190+ # Lightfold Docker container run script
191+ CONTAINER_NAME="lightfold-%s"
192+ IMAGE_NAME="%s"
193+ RELEASE_PATH="%s"
194+
195+ # Stop and remove existing container if running
196+ docker stop $CONTAINER_NAME 2>/dev/null || true
197+ docker rm $CONTAINER_NAME 2>/dev/null || true
198+
199+ # Run new container
200+ docker run -d \
201+ --name $CONTAINER_NAME \
202+ --restart unless-stopped \
203+ -p %s:3000 \
204+ --env-file $RELEASE_PATH/.env \
205+ $IMAGE_NAME
206+ ` , appName , imageName , opts .ReleasePath , port )
207+
208+ runScriptPath := fmt .Sprintf ("%s/docker-run.sh" , opts .ReleasePath )
209+ if err := ssh .WriteRemoteFile (runScriptPath , runScript , 0755 ); err != nil {
210+ return & builders.BuildResult {
211+ Success : false ,
212+ BuildLog : buildLog .String (),
213+ }, fmt .Errorf ("failed to write docker run script: %w" , err )
214+ }
215+
216+ ssh .ExecuteSudo (fmt .Sprintf ("chown deploy:deploy %s" , runScriptPath ))
217+ buildLog .WriteString (fmt .Sprintf ("\n Created Docker run script at %s\n " , runScriptPath ))
218+
219+ buildLog .WriteString ("\n ✓ Docker build completed successfully\n " )
47220
48221 return & builders.BuildResult {
49- Success : false ,
50- BuildLog : "" ,
51- IncludesNginx : true ,
52- }, fmt .Errorf ("dockerfile builder not yet implemented - requires container runtime support" )
222+ Success : true ,
223+ BuildLog : buildLog .String (),
224+ IncludesNginx : false , // Docker containers typically include their own web server
225+ StartCommand : fmt .Sprintf ("bash %s/docker-run.sh" , opts .ReleasePath ),
226+ }, nil
227+ }
228+
229+ // extractAppName extracts the app name from the release path
230+ // Example: /srv/myapp/releases/20240101120000 -> myapp
231+ func extractAppName (releasePath string ) string {
232+ parts := strings .Split (releasePath , "/" )
233+ if len (parts ) >= 3 {
234+ // Path format: /srv/{appname}/releases/{timestamp}
235+ return parts [2 ]
236+ }
237+ return ""
238+ }
239+
240+ // extractPortFromEnv tries to find PORT env var, defaults to ""
241+ func extractPortFromEnv (envVars map [string ]string ) string {
242+ if port , ok := envVars ["PORT" ]; ok {
243+ return port
244+ }
245+ return ""
53246}
0 commit comments