@@ -43,7 +43,7 @@ func (s *Server) runStartupMigrations() {
4343 s .ensurePMAConfig ()
4444 s .ensureMySQLTuning ()
4545 s .ensureSwap ()
46- s .ensureUserSocketDirs ()
46+ s .bootstrapAllUsers ()
4747 s .cleanStaleSocketsAndReload ()
4848}
4949
@@ -166,65 +166,28 @@ func (s *Server) ensureServiceFiles() {
166166 }
167167}
168168
169- func (s * Server ) ensureUserSocketDirs () {
170- // Check if any old-style sockets exist in the shared dir -- if so, we need to migrate
169+ // bootstrapAllUsers ensures every user that has a config directory also has
170+ // all required runtime directories with correct ownership. Also migrates
171+ // away from the legacy shared-socket layout if still present.
172+ func (s * Server ) bootstrapAllUsers () {
173+ // Migrate away from old shared /opt/fastcp/run/php-*.sock layout
171174 oldSockets , _ := filepath .Glob (filepath .Join (fastcpRunDir , "php-*.sock" ))
172- if len (oldSockets ) == 0 {
173- // Also check if any user config dirs exist without the new socket dir
174- userDirs , _ := filepath .Glob ("/opt/fastcp/config/users/*" )
175- needsMigration := false
176- for _ , dir := range userDirs {
177- username := filepath .Base (dir )
178- sockDir := userSocketDir (username )
179- if _ , err := os .Stat (sockDir ); err != nil {
180- needsMigration = true
181- break
182- }
175+ if len (oldSockets ) > 0 {
176+ slog .Info ("migrating legacy shared sockets to per-user home directories" )
177+ for _ , sock := range oldSockets {
178+ os .Remove (sock )
183179 }
184- if ! needsMigration {
185- return
180+ oldPids , _ := filepath .Glob (filepath .Join (fastcpRunDir , "php-*.pid" ))
181+ for _ , pid := range oldPids {
182+ os .Remove (pid )
186183 }
184+ exec .Command ("pkill" , "-f" , "frankenphp run --config /opt/fastcp/config/users" ).Run ()
185+ time .Sleep (1 * time .Second )
187186 }
188187
189- slog .Info ("migrating user socket directories to home directories" )
190-
191- // Create ~/.fastcp/run/ for all existing users with config dirs
192188 userDirs , _ := filepath .Glob ("/opt/fastcp/config/users/*" )
193189 for _ , dir := range userDirs {
194- username := filepath .Base (dir )
195- u , err := user .Lookup (username )
196- if err != nil {
197- continue
198- }
199- uid , _ := strconv .Atoi (u .Uid )
200- gid , _ := strconv .Atoi (u .Gid )
201-
202- sockDir := userSocketDir (username )
203- os .MkdirAll (sockDir , 0755 )
204- os .Chown (sockDir , uid , gid )
205- // Also ensure parent .fastcp dir has correct ownership
206- os .Chown (filepath .Dir (sockDir ), uid , gid )
207- }
208-
209- // Remove old sockets so user PHP processes restart with new paths
210- for _ , sock := range oldSockets {
211- os .Remove (sock )
212- }
213- oldPids , _ := filepath .Glob (filepath .Join (fastcpRunDir , "php-*.pid" ))
214- for _ , pid := range oldPids {
215- os .Remove (pid )
216- }
217-
218- // Kill old user FrankenPHP processes so they restart with new socket paths
219- exec .Command ("pkill" , "-f" , "frankenphp run --config /opt/fastcp/config/users" ).Run ()
220- time .Sleep (1 * time .Second )
221-
222- // Regenerate Caddyfiles with new socket paths and reload
223- if err := s .generateCaddyfile (); err != nil {
224- slog .Error ("failed to regenerate Caddyfile during migration" , "error" , err )
225- } else {
226- s .reloadCaddy ()
227- slog .Info ("regenerated Caddyfile with new user socket paths" )
190+ bootstrapUserEnvironment (filepath .Base (dir ))
228191 }
229192}
230193
@@ -876,17 +839,46 @@ func userSocketPath(username string) string {
876839 return filepath .Join (userSocketDir (username ), "php.sock" )
877840}
878841
879- func ensureUserSocketDir (username string ) {
880- sockDir := userSocketDir (username )
842+ // bootstrapUserEnvironment creates all required directories and fixes
843+ // ownership for a system user. Safe to call repeatedly (idempotent).
844+ func bootstrapUserEnvironment (username string ) {
881845 u , err := user .Lookup (username )
882846 if err != nil {
883847 return
884848 }
885849 uid , _ := strconv .Atoi (u .Uid )
886850 gid , _ := strconv .Atoi (u .Gid )
887- os .MkdirAll (sockDir , 0755 )
888- os .Chown (sockDir , uid , gid )
889- os .Chown (filepath .Dir (sockDir ), uid , gid )
851+
852+ own := func (p string ) { os .Chown (p , uid , gid ) }
853+ mkown := func (p string , mode os.FileMode ) {
854+ os .MkdirAll (p , mode )
855+ own (p )
856+ }
857+
858+ // ~/apps/
859+ mkown (filepath .Join (u .HomeDir , appsDir ), 0755 )
860+
861+ // ~/.fastcp/ and ~/.fastcp/run/ (socket directory)
862+ fastcpPath := filepath .Join (u .HomeDir , fastcpDir )
863+ mkown (fastcpPath , 0755 )
864+ mkown (filepath .Join (fastcpPath , "run" ), 0755 )
865+
866+ // ~/.tmp/ tree (sessions, uploads, cache, phpmyadmin, wsdl)
867+ tmpDir := filepath .Join (u .HomeDir , ".tmp" )
868+ for _ , sub := range []string {"" , "sessions" , "uploads" , "cache" , "phpmyadmin" , "wsdl" } {
869+ mkown (filepath .Join (tmpDir , sub ), 0700 )
870+ }
871+
872+ // /opt/fastcp/config/users/{username}/
873+ userConfigDir := filepath .Join ("/opt/fastcp/config/users" , username )
874+ os .MkdirAll (userConfigDir , 0755 )
875+
876+ // PHP error log
877+ logPath := fmt .Sprintf ("/var/log/fastcp/php-%s-error.log" , username )
878+ if _ , err := os .Stat (logPath ); err != nil {
879+ os .WriteFile (logPath , nil , 0644 )
880+ }
881+ own (logPath )
890882}
891883
892884func (s * Server ) startUserPHP (username string ) error {
@@ -1113,7 +1105,7 @@ func (s *Server) generateCaddyfile() error {
11131105
11141106 // Start user's PHP service (skip if suspended)
11151107 if len (sites ) > 0 && ! isSuspended {
1116- ensureUserSocketDir (username )
1108+ bootstrapUserEnvironment (username )
11171109 if useSystemd {
11181110 serviceName := fmt .Sprintf ("fastcp-php@%s.service" , username )
11191111 exec .Command ("systemctl" , "start" , serviceName ).Run ()
@@ -1286,45 +1278,20 @@ func (s *Server) generateUserCaddyfile(username string, sites []siteInfo) error
12861278` , matcherName , hostList , matcherName , site .DocumentRoot ))
12871279 }
12881280
1289- // Create per -user temp directory
1281+ // Per -user temp directory paths (directories are created by bootstrapUserEnvironment)
12901282 userTmpDir := filepath .Join (homeBase , username , ".tmp" )
12911283 userSessionDir := filepath .Join (userTmpDir , "sessions" )
12921284 userUploadDir := filepath .Join (userTmpDir , "uploads" )
1293- userPmaTmpDir := filepath .Join (userTmpDir , "phpmyadmin" )
1294-
1295- // Get user info for ownership
1296- u , err := user .Lookup (username )
1297- if err == nil {
1298- uid , _ := strconv .Atoi (u .Uid )
1299- gid , _ := strconv .Atoi (u .Gid )
1300-
1301- for _ , dir := range []string {userTmpDir , userSessionDir , userUploadDir , userPmaTmpDir } {
1302- os .MkdirAll (dir , 0700 )
1303- os .Chown (dir , uid , gid )
1304- }
1305- }
1285+ userCacheDir := filepath .Join (userTmpDir , "cache" )
1286+ userWsdlDir := filepath .Join (userTmpDir , "wsdl" )
13061287
1307- // Create per-user php.ini with security settings
13081288 var docRoots []string
13091289 for _ , site := range sites {
13101290 docRoots = append (docRoots , filepath .Dir (site .DocumentRoot ))
13111291 docRoots = append (docRoots , site .DocumentRoot )
13121292 }
1313- // Include user's own temp directory, NOT shared /tmp
13141293 openBasedir := strings .Join (docRoots , ":" ) + ":" + userTmpDir + ":/opt/fastcp/phpmyadmin"
13151294
1316- // Additional cache directories
1317- userCacheDir := filepath .Join (userTmpDir , "cache" )
1318- userWsdlDir := filepath .Join (userTmpDir , "wsdl" )
1319- os .MkdirAll (userCacheDir , 0700 )
1320- os .MkdirAll (userWsdlDir , 0700 )
1321- if err == nil {
1322- uid , _ := strconv .Atoi (u .Uid )
1323- gid , _ := strconv .Atoi (u .Gid )
1324- os .Chown (userCacheDir , uid , gid )
1325- os .Chown (userWsdlDir , uid , gid )
1326- }
1327-
13281295 phpIni := fmt .Sprintf (`; PHP security settings for user: %s
13291296; Isolation: Each user has their own temp/session/cache directories
13301297; Generated by FastCP
@@ -1386,7 +1353,7 @@ session.cookie_samesite = Strict
13861353 }
13871354
13881355 // Reload user's FrankenPHP service
1389- ensureUserSocketDir (username )
1356+ bootstrapUserEnvironment (username )
13901357 serviceName := fmt .Sprintf ("fastcp-php@%s.service" , username )
13911358 exec .Command ("systemctl" , "reload-or-restart" , serviceName ).Run ()
13921359
@@ -1876,29 +1843,14 @@ func (s *Server) handleCreateUser(ctx context.Context, params json.RawMessage) (
18761843 return nil , fmt .Errorf ("failed to set password: %w: %s" , err , output )
18771844 }
18781845
1879- // Create apps directory
1846+ // Bootstrap all directories and fix ownership
1847+ bootstrapUserEnvironment (req .Username )
1848+
18801849 u , _ := user .Lookup (req .Username )
18811850 uid , _ := strconv .Atoi (u .Uid )
1882- gid , _ := strconv .Atoi (u .Gid )
1883-
1884- appsPath := filepath .Join (u .HomeDir , appsDir )
1885- os .MkdirAll (appsPath , 0755 )
1886- os .Chown (appsPath , uid , gid )
1887-
1888- // Create .fastcp directory and runtime subdirectory
1889- fastcpPath := filepath .Join (u .HomeDir , fastcpDir )
1890- os .MkdirAll (fastcpPath , 0755 )
1891- os .Chown (fastcpPath , uid , gid )
1892-
1893- runPath := filepath .Join (fastcpPath , "run" )
1894- os .MkdirAll (runPath , 0755 )
1895- os .Chown (runPath , uid , gid )
1896-
1897- // Create per-user FrankenPHP configuration directory
1898- userConfigDir := filepath .Join ("/opt/fastcp/config/users" , req .Username )
1899- os .MkdirAll (userConfigDir , 0755 )
19001851
19011852 // Create user's Caddyfile (initially empty, will be populated when sites are created)
1853+ userConfigDir := filepath .Join ("/opt/fastcp/config/users" , req .Username )
19021854 userCaddyfile := filepath .Join (userConfigDir , "Caddyfile" )
19031855 initialCaddyfile := fmt .Sprintf (`# FrankenPHP config for user: %s
19041856{
0 commit comments